Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .downstream/gpt6-models-and-prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ ChatGPT 边缘自 2026-09-23 起不再为 `/backend-api/codex/responses` 的流
- 创建账号(数据导入、复制账号同此路径)时丢弃这类账号带入的 `extra.system_prompt`;批量编辑和 extra 合并接口在任何平台都不写该键,已有账号的绑定只由绑定接口写入。
- 迁移 `268_purge_system_prompt_bindings_without_insertion_point.sql` 一次性删除它们已存的绑定。

提示词库里 259 随旧默认规则一起带入的“先抓取远程规则文件”块已失效:它指向 `/skills/security-research/current/`,这条公开路由随远程 Skill 注册表在 #206 删除,现在落到前端回退页并返回 HTML,模型读不到 Markdown 原文。迁移 `269_strip_removed_remote_rules_bootstrap_from_system_prompts.sql` 只按形状删除该块(含该路径的代码围栏段、其后仍提到 `REMOTE_ROOT` 的段落,以及紧邻其前的简短抓取说明);路径出现在其他位置,或去掉该块后正文为空的提示词原样保留并输出通知。其余段落、库顺序、全局开关、默认提示词和其他设置不动,没有该路径的配置不写入,可重复执行。

请求路径只读内存快照(保存即替换,60 秒后台刷新以同步多实例)和调度账号自带的绑定,不查询数据库;配置不可用时不注入并告警,不向客户端返回错误。每个最终协议只注入一次:

| 最终协议 | 投递 |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//go:build integration

package repository

import (
"context"
"encoding/json"
"strings"
"testing"

dbmigrations "github.com/Wei-Shaw/sub2api/migrations"
"github.com/stretchr/testify/require"
)

const systemPromptRemoteBootstrapMigration = "269_strip_removed_remote_rules_bootstrap_from_system_prompts.sql"

// TestMain applied every migration, 269 included, to an empty database. The
// test stores libraries made of invented prompt text, applies 269, checks that
// only the fenced route block and its fetch lines go, that a reference it does
// not recognise is left as stored, and applies it again.
func TestMigration269StripsRemovedRemoteRulesBootstrap(t *testing.T) {
tx := testTx(t)
ctx := context.Background()
content, err := dbmigrations.FS.ReadFile(systemPromptRemoteBootstrapMigration)
require.NoError(t, err)
migration := string(content)

const route = "https://example.test/skills/security-research/current"
fence := "```text\nREMOTE_ROOT = " + route + "\n```"
paragraphs := func(parts ...string) string { return strings.Join(parts, "\n\n") }
inlineReference := "A rule that names " + route + "/ONE.md in the middle of a sentence."
prompt := func(id, body string) map[string]any {
return map[string]any{"id": id, "name": "Prompt " + id, "body": body, "position": "append", "role": "developer"}
}
library := func(prompts ...map[string]any) string {
raw, err := json.Marshal(map[string]any{"enabled": true, "default_prompt_id": "bootstrap", "prompts": prompts})
require.NoError(t, err)
return string(raw)
}
store := func(value string) {
t.Helper()
_, err := tx.ExecContext(ctx, `
INSERT INTO settings (key, value, updated_at) VALUES ('system_prompts', $1, TIMESTAMPTZ '2000-01-01 00:00:00+00')
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at`, value)
require.NoError(t, err)
}
stored := func() (value string, untouched bool) {
t.Helper()
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT value, updated_at = TIMESTAMPTZ '2000-01-01 00:00:00+00' FROM settings WHERE key = 'system_prompts'`).Scan(&value, &untouched))
return value, untouched
}

before := library(
prompt("bootstrap", paragraphs("Intro paragraph.", "# Working rules",
"Before work, fetch and read these cloud files first, in order:", fence,
"1. `REMOTE_ROOT/ONE.md`\n2. `REMOTE_ROOT/TWO.md`",
"If a cloud file cannot be read, say so. See REMOTE_ROOT/ONE.md.", "Closing line.")),
prompt("plain", paragraphs("Plain text with no route.", "Second paragraph.")),
prompt("inline", paragraphs(inlineReference, "Tail.")),
prompt("only-block", fence),
prompt("block-at-end", paragraphs("Keep me.", "Fetch the Cloud files:", fence)),
prompt("no-lead", paragraphs("Keep one.", fence, "Keep two.")),
)
after := library(
prompt("bootstrap", paragraphs("Intro paragraph.", "# Working rules", "Closing line.")),
prompt("plain", paragraphs("Plain text with no route.", "Second paragraph.")),
prompt("inline", paragraphs(inlineReference, "Tail.")),
prompt("only-block", fence),
prompt("block-at-end", "Keep me."),
prompt("no-lead", paragraphs("Keep one.", "Keep two.")),
)

store(before)
for run := 1; run <= 2; run++ {
_, err := tx.ExecContext(ctx, migration)
require.NoError(t, err, "run %d", run)
value, untouched := stored()
require.JSONEq(t, after, value, "run %d", run)
require.False(t, untouched, "run %d writes the changed library", run)
}

// A library that never named the route is not written, byte for byte.
clean := library(prompt("bootstrap", paragraphs("Plain text with no route.", "Second paragraph.")))
store(clean)
_, err = tx.ExecContext(ctx, migration)
require.NoError(t, err)
value, untouched := stored()
require.Equal(t, clean, value)
require.True(t, untouched, "a library without the route is not written")

// Text that is not a library is left as it is, and a missing row stays missing.
for _, other := range []string{"not json", `["prompts"]`, `{"enabled": true}`, `{"prompts": "x"}`} {
store(other)
_, err = tx.ExecContext(ctx, migration)
require.NoError(t, err, other)
value, untouched = stored()
require.Equal(t, other, value)
require.True(t, untouched, other)
}
_, err = tx.ExecContext(ctx, `DELETE FROM settings WHERE key = 'system_prompts'`)
require.NoError(t, err)
_, err = tx.ExecContext(ctx, migration)
require.NoError(t, err)
var rows int
require.NoError(t, tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM settings WHERE key = 'system_prompts'`).Scan(&rows))
require.Zero(t, rows)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
-- Remove the retired remote-rules bootstrap from the stored system prompt library.
--
-- The legacy default rule carried a block that told the model to fetch three
-- files under <host>/skills/security-research/current/ before it worked. That
-- public route belonged to the remote Skill registry, which release #206
-- deleted, and 259 kept the rule's inline text as a library prompt without the
-- registry. The path now falls through to the single-page front end and answers
-- 200 text/html, so a client that follows the instruction reads an HTML page
-- where it was promised Markdown and reports that it cannot read the rules.
--
-- Only that block goes. It is recognised by shape, not by its words: a fenced
-- paragraph that names the removed route, the paragraphs directly after it that
-- still mention REMOTE_ROOT (the file list and the fallback note), and the one
-- paragraph directly before it when that paragraph is the short fetch
-- instruction. A prompt whose route reference sits anywhere else, for example
-- inside a long paragraph, or whose body would be empty without the block, is
-- left as stored and reported with a notice. Every other paragraph, the order
-- of the library, the enabled switch, the default prompt and every other
-- setting are not touched; a library without the route is not written.
--
-- Running the file again is a no-op. A missing, empty or unparsable
-- system_prompts row is left alone.
-- No explicit BEGIN/COMMIT: the runner owns this file's transaction.
SET LOCAL lock_timeout = '10s';
SET LOCAL statement_timeout = '120s';

DO $strip_remote_bootstrap$
DECLARE
dead_path CONSTANT TEXT := '/skills/security-research/current';
stored TEXT;
config JSONB;
item JSONB;
out_items JSONB := '[]'::JSONB;
body TEXT;
new_body TEXT;
paras TEXT[];
total INT;
anchor INT;
cut_from INT;
cut_to INT;
idx INT;
changed BOOLEAN := FALSE;
remaining INT := 0;
BEGIN
SELECT value INTO stored FROM settings WHERE key = 'system_prompts';
IF stored IS NULL OR btrim(stored) = '' THEN
RETURN;
END IF;
BEGIN
config := stored::JSONB;
EXCEPTION WHEN others THEN
RAISE NOTICE '269: system_prompts is not valid JSON, left unchanged';
RETURN;
END;
IF jsonb_typeof(config) IS DISTINCT FROM 'object'
OR jsonb_typeof(config->'prompts') IS DISTINCT FROM 'array' THEN
RETURN;
END IF;

FOR item IN SELECT elem FROM jsonb_array_elements(config->'prompts') AS elem LOOP
IF jsonb_typeof(item) = 'object'
AND jsonb_typeof(item->'body') = 'string'
AND strpos(item->>'body', dead_path) > 0 THEN
body := item->>'body';
LOOP
paras := string_to_array(body, E'\n\n');
total := COALESCE(array_length(paras, 1), 0);
anchor := 0;
FOR idx IN 1..total LOOP
IF strpos(paras[idx], dead_path) > 0 THEN
anchor := idx;
EXIT;
END IF;
END LOOP;
EXIT WHEN anchor = 0;

IF left(paras[anchor], 3) <> '```' OR right(paras[anchor], 3) <> '```'
OR length(paras[anchor]) > 500 THEN
RAISE NOTICE '269: prompt % keeps a route reference outside a fenced block', item->>'id';
EXIT;
END IF;

cut_from := anchor;
IF anchor > 1 AND length(paras[anchor - 1]) <= 400
AND paras[anchor - 1] ILIKE '%fetch%' AND paras[anchor - 1] ILIKE '%cloud%' THEN
cut_from := anchor - 1;
END IF;
cut_to := anchor;
WHILE cut_to < total
AND length(paras[cut_to + 1]) <= 600
AND (strpos(paras[cut_to + 1], 'REMOTE_ROOT') > 0
OR strpos(paras[cut_to + 1], dead_path) > 0) LOOP
cut_to := cut_to + 1;
END LOOP;

new_body := array_to_string(paras[1:cut_from - 1] || paras[cut_to + 1:total], E'\n\n');
IF btrim(new_body) = '' THEN
RAISE NOTICE '269: prompt % would be empty without the block, left as stored', item->>'id';
EXIT;
END IF;
body := new_body;
END LOOP;

IF body IS DISTINCT FROM item->>'body' THEN
item := jsonb_set(item, '{body}', to_jsonb(body));
changed := TRUE;
END IF;
IF strpos(body, dead_path) > 0 THEN
remaining := remaining + 1;
END IF;
END IF;
out_items := out_items || jsonb_build_array(item);
END LOOP;

IF changed THEN
UPDATE settings
SET value = jsonb_set(config, '{prompts}', out_items)::TEXT, updated_at = NOW()
WHERE key = 'system_prompts';
END IF;
RAISE NOTICE '269: system_prompts changed=%, prompts still naming the removed route=%', changed, remaining;
END;
$strip_remote_bootstrap$;
Loading