@gavinc-cn/touchstone-dsh 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -11
- package/README.zh.md +25 -10
- package/backend/lib/access.js +44 -0
- package/backend/lib/agents.js +341 -0
- package/backend/lib/args.js +94 -0
- package/backend/lib/auth.js +195 -0
- package/backend/lib/board.js +6154 -0
- package/backend/lib/builtin-assets.js +1177 -0
- package/backend/lib/casescan.js +456 -0
- package/backend/lib/chat.js +2053 -0
- package/backend/lib/cookie.js +76 -0
- package/backend/lib/db.js +2260 -0
- package/backend/lib/dshdriver.js +1148 -0
- package/backend/lib/dshevents.js +851 -0
- package/backend/lib/export-cases.js +261 -0
- package/backend/lib/feishu.js +1738 -0
- package/backend/lib/feishu_cfg.js +228 -0
- package/backend/lib/feishu_conv.js +1484 -0
- package/backend/lib/feishu_device.js +428 -0
- package/backend/lib/feishu_inbound.js +1674 -0
- package/backend/lib/feishu_provision.js +855 -0
- package/backend/lib/feishu_rest.js +398 -0
- package/backend/lib/feishu_slash.js +568 -0
- package/backend/lib/feishu_ws.js +950 -0
- package/backend/lib/http.js +312 -0
- package/backend/lib/lib.js +973 -0
- package/backend/lib/loadcase.js +749 -0
- package/backend/lib/loadgen.js +1407 -0
- package/backend/lib/localbus.js +164 -0
- package/backend/lib/lock.js +405 -0
- package/backend/lib/proc.js +278 -0
- package/backend/lib/prompts.js +1021 -0
- package/backend/lib/pybase64.js +114 -0
- package/backend/lib/pyjson.js +214 -0
- package/backend/lib/query.js +125 -0
- package/backend/lib/rag.js +1310 -0
- package/backend/lib/runner.js +4520 -0
- package/backend/lib/servercaps.js +182 -0
- package/backend/lib/serverstate.js +237 -0
- package/backend/lib/sessparse.js +1176 -0
- package/backend/lib/sse.js +207 -0
- package/backend/lib/static.js +84 -0
- package/backend/lib/taskjson.js +302 -0
- package/backend/lib/waitq.js +2114 -0
- package/backend/lib/worktree.js +588 -0
- package/backend/lib/zstd-frames.js +218 -0
- package/backend/routes/admin.js +553 -0
- package/backend/routes/agents.js +149 -0
- package/backend/routes/assets.js +145 -0
- package/backend/routes/board.js +1220 -0
- package/backend/routes/feishu.js +269 -0
- package/backend/routes/feishu_app.js +215 -0
- package/backend/routes/feishu_hook.js +358 -0
- package/backend/routes/index.js +297 -0
- package/backend/routes/projects.js +1059 -0
- package/backend/routes/sessions.js +946 -0
- package/backend/routes/tasks.js +1530 -0
- package/backend/schema/fixtures/ancient.sql +189 -0
- package/backend/schema/fixtures/pre_alter.sql +177 -0
- package/backend/schema/migrations.json +154 -0
- package/backend/schema/schema.json +1451 -0
- package/backend/schema/schema.sql +216 -0
- package/backend/server.js +498 -0
- package/board.py +805 -72
- package/chat.py +590 -17
- package/db.py +356 -140
- package/dsh-plugin/cordis.patch.yml +7 -3
- package/dsh-plugin/install.sh +28 -11
- package/dsh-plugin/lib/agent-driver.js +710 -89
- package/dsh-plugin/lib/client.bundle.js +94 -9
- package/dsh-plugin/lib/index.js +529 -39
- package/dsh-plugin/lib/pack-closure.mjs +291 -0
- package/dsh-plugin/scripts/prepack-check.mjs +30 -2
- package/dsh-plugin/src/client.js +94 -9
- package/dshdriver.py +74 -5
- package/dshevents.py +65 -3
- package/feishu.py +1197 -34
- package/feishu_conv.py +920 -0
- package/lifecycle.py +116 -13
- package/package.json +6 -1
- package/requirements.txt +2 -1
- package/server.py +218 -26
- package/sessparse.py +150 -44
- package/touchstone.py +6 -3
- package/waitq.py +23 -0
- package/webui/dist-plugin/assets/index-ChP3sg6y.css +1 -0
- package/webui/dist-plugin/assets/index-UrMyeAXF.js +484 -0
- package/webui/dist-plugin/index.html +2 -2
- package/webui/dist-plugin/assets/index-B0WoP1Qy.css +0 -1
- package/webui/dist-plugin/assets/index-DtwKoi1T.js +0 -473
package/db.py
CHANGED
|
@@ -206,11 +206,18 @@ CREATE TABLE IF NOT EXISTS feishu_user_cfgs (
|
|
|
206
206
|
user_id INTEGER PRIMARY KEY,
|
|
207
207
|
json TEXT NOT NULL DEFAULT '{}'
|
|
208
208
|
);
|
|
209
|
+
-- 飞书号绑定(一行 = 一个飞书号 ↔ 一个 Touchstone 用户);后四列是「当前会话」
|
|
210
|
+
-- 绑定态(2026-10-08 飞书通用对话):私聊普通文本按它路由到 agent 会话,
|
|
211
|
+
-- 换绑/解绑一律归零(见 set_feishu_binding 的「换绑归零」语义)
|
|
209
212
|
CREATE TABLE IF NOT EXISTS feishu_bindings (
|
|
210
213
|
open_id TEXT PRIMARY KEY,
|
|
211
214
|
user_id INTEGER NOT NULL UNIQUE,
|
|
212
215
|
default_project_id INTEGER NOT NULL DEFAULT 0,
|
|
213
|
-
bound_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
|
216
|
+
bound_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
|
217
|
+
cur_sid TEXT NOT NULL DEFAULT '', -- 当前会话 id(空=未绑定会话)
|
|
218
|
+
cur_project_id INTEGER NOT NULL DEFAULT 0, -- 当前会话所属项目(投递队列归属)
|
|
219
|
+
cur_title TEXT NOT NULL DEFAULT '', -- 绑定时标题快照
|
|
220
|
+
cur_bound_at TEXT NOT NULL DEFAULT '' -- 绑定时刻(localtime)
|
|
214
221
|
);
|
|
215
222
|
CREATE INDEX IF NOT EXISTS idx_board_cards_project ON board_cards(project_id);
|
|
216
223
|
CREATE INDEX IF NOT EXISTS idx_board_comments_card ON board_comments(card_id);
|
|
@@ -268,6 +275,285 @@ def now_str():
|
|
|
268
275
|
return time.strftime("%Y-%m-%d %H:%M:%S")
|
|
269
276
|
|
|
270
277
|
|
|
278
|
+
# --------------------------------------------------------------------------
|
|
279
|
+
# 声明式迁移清单(数据级 / 结构重建)——**两侧唯一真源**(M1 地基)
|
|
280
|
+
# --------------------------------------------------------------------------
|
|
281
|
+
# 背景(2026-10-11,Node 后端重写 M1):Node 后端要与 Python 后端**开同一个库文件**,
|
|
282
|
+
# 两边必须执行同一套迁移。分两类:
|
|
283
|
+
# ① DDL 补列(ALTER TABLE ADD COLUMN):不共享清单——Node 侧用「向导出的目标 schema
|
|
284
|
+
# 收敛」的通用做法(目标由 tools/export_schema.py 从本文件实测导出);
|
|
285
|
+
# ② 数据级迁移与表重建(本清单):**无法从 schema 推导**,必须逐条共享,
|
|
286
|
+
# Python 与 Node 执行同一份 backend/schema/migrations.json。
|
|
287
|
+
#
|
|
288
|
+
# 导出与漂移守卫:`python3 tools/export_schema.py`(改本清单后必须重跑),
|
|
289
|
+
# pytest `tests/test_schema_export.py` 断言「重新导出 == 已提交文件」。
|
|
290
|
+
#
|
|
291
|
+
# 每步字段:
|
|
292
|
+
# id : 稳定标识(日期前缀 + 语义)。语义变了就换新 id,不要改老 id 的含义。
|
|
293
|
+
# desc : 一句话说明(写进 migrations.json,两侧共用同一份文案)。
|
|
294
|
+
# when : 运行条件;None = 无条件执行(语句自身必须幂等)
|
|
295
|
+
# {"kind": "rows", "sql": "<SELECT>"} → 该 SELECT 返回 ≥1 行才执行
|
|
296
|
+
# {"kind": "index_origin_unique", "table": "t"} → 表上存在 UNIQUE 约束索引
|
|
297
|
+
# (PRAGMA index_list 的
|
|
298
|
+
# origin='u')才执行
|
|
299
|
+
# sql : 语句列表(按序执行,单步内共用一次事务)
|
|
300
|
+
# handler : 需宿主语言实现的步骤名(Python: MIGRATION_HANDLERS;Node 同名实现);
|
|
301
|
+
# 与 sql 二选一。**未知 handler 一律报错退出**,绝不静默跳过。
|
|
302
|
+
#
|
|
303
|
+
# 顺序即执行顺序(列表序 = 事务序)。表重建必须在补列**之后**:Python 的旧实现就是
|
|
304
|
+
# 「先 ALTER 补列、再重建」,重建的 INSERT…SELECT 只搬 12 个老列,于是那次重建会把
|
|
305
|
+
# 后加的列丢掉、下次启动由补列重新加回(存量极老库的一次性现象,两侧必须一致)。
|
|
306
|
+
MIGRATIONS = [
|
|
307
|
+
{
|
|
308
|
+
"id": "2026-09-04_projects_guide_text_merge",
|
|
309
|
+
"desc": "废弃列 commit_spec/deploy_spec 的内容并入 guide_text 后清空两列",
|
|
310
|
+
"when": {"kind": "rows",
|
|
311
|
+
"sql": "SELECT 1 FROM projects WHERE trim(coalesce(commit_spec,'')) != ''"
|
|
312
|
+
" OR trim(coalesce(deploy_spec,'')) != '' LIMIT 1"},
|
|
313
|
+
"handler": "projects_guide_text_merge",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"id": "2026-09-04_projects_user_id_backfill",
|
|
317
|
+
"desc": "多用户隔离:存量项目(user_id=0)归 admin",
|
|
318
|
+
"when": {"kind": "rows", "sql": "SELECT 1 FROM projects WHERE user_id=0 LIMIT 1"},
|
|
319
|
+
"sql": ["UPDATE projects SET user_id="
|
|
320
|
+
" COALESCE((SELECT id FROM users WHERE username='admin'), 0)"
|
|
321
|
+
" WHERE user_id=0"],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"id": "2026-09-13_board_cards_clear_commit_block_text",
|
|
325
|
+
"desc": "阻塞让行提交退场:清理存量卡片上的机器文案 block_text",
|
|
326
|
+
"when": {"kind": "rows",
|
|
327
|
+
"sql": "SELECT 1 FROM board_cards WHERE block_text LIKE '提交中:%'"
|
|
328
|
+
" OR block_text LIKE '提交失败:%' LIMIT 1"},
|
|
329
|
+
"sql": ["UPDATE board_cards SET block_text=''"
|
|
330
|
+
" WHERE block_text LIKE '提交中:%' OR block_text LIKE '提交失败:%'"],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"id": "2026-09-13_board_cards_clear_commit_errors",
|
|
334
|
+
"desc": "阻塞让行提交退场:清理存量卡片上的提交失败 last_error",
|
|
335
|
+
"when": {"kind": "rows",
|
|
336
|
+
"sql": "SELECT 1 FROM board_cards WHERE last_error LIKE '阻塞让行提交失败%'"
|
|
337
|
+
" OR last_error LIKE '阻塞让行提交异常%' LIMIT 1"},
|
|
338
|
+
"sql": ["UPDATE board_cards SET last_error='', last_error_at=NULL"
|
|
339
|
+
" WHERE last_error LIKE '阻塞让行提交失败%'"
|
|
340
|
+
" OR last_error LIKE '阻塞让行提交异常%'"],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"id": "2026-09-04_projects_drop_global_unique_name",
|
|
344
|
+
"desc": "旧库 projects.name 全局 UNIQUE → (user_id,name) 用户内唯一:重建表",
|
|
345
|
+
"when": {"kind": "index_origin_unique", "table": "projects"},
|
|
346
|
+
# 语句照抄旧实现:只搬 12 个老列(后加的 model/archived/skill_* 五列/
|
|
347
|
+
# reasoning_effort/permission_mode 共 9 列不在其中,丢列由下次启动的补列重新加回
|
|
348
|
+
# ——两侧一致;ancient 夹具与对拍器都按「两轮快照」钉住这条)
|
|
349
|
+
"sql": [
|
|
350
|
+
"ALTER TABLE projects RENAME TO projects_old",
|
|
351
|
+
"""CREATE TABLE projects (
|
|
352
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
353
|
+
user_id INTEGER NOT NULL DEFAULT 0,
|
|
354
|
+
name TEXT NOT NULL,
|
|
355
|
+
project_dir TEXT NOT NULL,
|
|
356
|
+
agent_path TEXT NOT NULL DEFAULT '',
|
|
357
|
+
work_dir TEXT NOT NULL,
|
|
358
|
+
bug_dir TEXT NOT NULL,
|
|
359
|
+
guide_text TEXT NOT NULL DEFAULT '',
|
|
360
|
+
commit_spec TEXT NOT NULL DEFAULT '',
|
|
361
|
+
deploy_spec TEXT NOT NULL DEFAULT '',
|
|
362
|
+
env_label TEXT NOT NULL DEFAULT '',
|
|
363
|
+
created_at TEXT NOT NULL
|
|
364
|
+
)""",
|
|
365
|
+
"""INSERT INTO projects(id, user_id, name, project_dir, agent_path, work_dir, bug_dir,
|
|
366
|
+
guide_text, commit_spec, deploy_spec, env_label, created_at)
|
|
367
|
+
SELECT id, user_id, name, project_dir, agent_path, work_dir, bug_dir,
|
|
368
|
+
guide_text, commit_spec, deploy_spec, env_label, created_at
|
|
369
|
+
FROM projects_old""",
|
|
370
|
+
"DROP TABLE projects_old",
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"id": "2026-09-04_projects_user_name_unique_index",
|
|
375
|
+
"desc": "补建用户内唯一索引 idx_projects_user_name(重建之后执行)",
|
|
376
|
+
"when": None,
|
|
377
|
+
"sql": ["CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_user_name"
|
|
378
|
+
" ON projects(user_id, name)"],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"id": "2026-08-30_projects_work_dir_from_legacy_cases_root",
|
|
382
|
+
"desc": "工作目录化:旧 work_dir(案例库根)改存工作目录,bug_dir 列废弃置空",
|
|
383
|
+
"when": {"kind": "rows", "sql": "SELECT 1 FROM projects WHERE bug_dir != '' LIMIT 1"},
|
|
384
|
+
"handler": "projects_work_dir_from_legacy_cases_root",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"id": "2026-09-19_board_cards_queue_state_doing",
|
|
388
|
+
"desc": "P4 排队占位单态化:blocked+queue 归位 doing+queue",
|
|
389
|
+
"when": {"kind": "rows",
|
|
390
|
+
"sql": "SELECT 1 FROM board_cards WHERE block_kind='queue'"
|
|
391
|
+
" AND column_key='blocked' LIMIT 1"},
|
|
392
|
+
"sql": ["UPDATE board_cards SET column_key='doing'"
|
|
393
|
+
" WHERE block_kind='queue' AND column_key='blocked'"],
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"id": "2026-09-09_feishu_appsettings_bridge",
|
|
397
|
+
"desc": "飞书配置用户级化:旧全局 app_settings['feishu'] 迁给 admin 的用户级配置",
|
|
398
|
+
"when": {"kind": "rows",
|
|
399
|
+
"sql": "SELECT 1 FROM app_settings WHERE key='feishu' LIMIT 1"},
|
|
400
|
+
"handler": "feishu_appsettings_bridge",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"id": "2026-09-24_drop_leases",
|
|
404
|
+
"desc": "v3d 租约层删除:DROP 存量 leases 表与索引(幂等)",
|
|
405
|
+
"when": None,
|
|
406
|
+
"sql": ["DROP TABLE IF EXISTS leases", "DROP INDEX IF EXISTS idx_leases_project"],
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"id": "2026-09-21_wait_items_claimed_to_starting",
|
|
410
|
+
"desc": "v2a 状态机扩七枚举:claimed 行归一 starting",
|
|
411
|
+
"when": {"kind": "rows", "sql": "SELECT 1 FROM wait_items WHERE state='claimed' LIMIT 1"},
|
|
412
|
+
"sql": ["UPDATE wait_items SET state='starting' WHERE state='claimed'"],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
"id": "2026-09-21_wait_items_rebuild_active_index",
|
|
416
|
+
"desc": "活跃唯一索引口径扩到四活跃态(旧库 WHERE 仍含 claimed 时重建)",
|
|
417
|
+
"when": {"kind": "rows",
|
|
418
|
+
"sql": "SELECT 1 FROM sqlite_master WHERE type='index'"
|
|
419
|
+
" AND name='idx_wait_active'"
|
|
420
|
+
" AND coalesce(sql,'') NOT LIKE '%''starting''%' LIMIT 1"},
|
|
421
|
+
"sql": [
|
|
422
|
+
"DROP INDEX idx_wait_active",
|
|
423
|
+
"CREATE UNIQUE INDEX idx_wait_active ON wait_items(kind, target_id)"
|
|
424
|
+
" WHERE state IN ('waiting','starting','running','finishing')",
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"id": "2026-10-11_schema_version_stamp",
|
|
429
|
+
"desc": "M1:把 schema 版本号写入 app_settings['schema_version'](两侧对拍前置断言)",
|
|
430
|
+
"when": None,
|
|
431
|
+
"sql": ["INSERT INTO app_settings(key, json) VALUES('schema_version', '1')"
|
|
432
|
+
" ON CONFLICT(key) DO UPDATE SET json=excluded.json"],
|
|
433
|
+
},
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
# schema 版本号(与 migrations.json 的 schemaVersion 同源;两侧对拍前先断言相等)
|
|
437
|
+
SCHEMA_VERSION = 1
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _mh_projects_guide_text_merge(conn):
|
|
441
|
+
"""handler:废弃列 commit_spec/deploy_spec 并入 guide_text 后清空(幂等)。"""
|
|
442
|
+
rows = conn.execute(
|
|
443
|
+
"SELECT id, guide_text, commit_spec, deploy_spec FROM projects").fetchall()
|
|
444
|
+
for r in rows:
|
|
445
|
+
extra = []
|
|
446
|
+
if (r["commit_spec"] or "").strip():
|
|
447
|
+
extra.append("git commit message 规范:" + r["commit_spec"].strip())
|
|
448
|
+
if (r["deploy_spec"] or "").strip():
|
|
449
|
+
extra.append("部署说明:" + r["deploy_spec"].strip())
|
|
450
|
+
if extra:
|
|
451
|
+
merged = (r["guide_text"] or "").rstrip()
|
|
452
|
+
merged = merged + "\n\n" + "\n".join(extra) if merged else "\n".join(extra)
|
|
453
|
+
conn.execute(
|
|
454
|
+
"UPDATE projects SET guide_text=?, commit_spec='', deploy_spec=''"
|
|
455
|
+
" WHERE id=?", (merged, r["id"]))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _mh_projects_work_dir_from_legacy_cases_root(conn):
|
|
459
|
+
"""handler:旧 work_dir(案例库根)改存工作目录;bug_dir 置空(幂等标记=bug_dir 非空)。
|
|
460
|
+
|
|
461
|
+
旧 work_dir 以 free_style 结尾则取其父目录,否则回落 <项目目录>/.touchstone;
|
|
462
|
+
磁盘文件一律不动。取父目录分支派生路径与原值一致;回落分支按新版默认目录派生。
|
|
463
|
+
"""
|
|
464
|
+
for r in conn.execute(
|
|
465
|
+
"SELECT id, project_dir, work_dir FROM projects"
|
|
466
|
+
" WHERE bug_dir != ''").fetchall():
|
|
467
|
+
old = (r["work_dir"] or "").strip().rstrip("/")
|
|
468
|
+
if old.endswith("/" + CASES_SUBDIR):
|
|
469
|
+
new_w = os.path.dirname(old)
|
|
470
|
+
else:
|
|
471
|
+
new_w = os.path.join(r["project_dir"] or "~", ".touchstone")
|
|
472
|
+
conn.execute("UPDATE projects SET work_dir=?, bug_dir='' WHERE id=?",
|
|
473
|
+
(new_w, r["id"]))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _mh_feishu_appsettings_bridge(conn):
|
|
477
|
+
"""handler:旧全局飞书配置 app_settings['feishu'] → admin 的用户级配置。
|
|
478
|
+
|
|
479
|
+
已有用户级配置则不覆盖;坏 JSON 视为空配置(与旧实现逐字一致)。注意只 INSERT、
|
|
480
|
+
不删 app_settings 行,故本步长期可重入(有用户级配置后恒为 no-op)。
|
|
481
|
+
"""
|
|
482
|
+
row = conn.execute(
|
|
483
|
+
"SELECT json FROM app_settings WHERE key='feishu'").fetchone()
|
|
484
|
+
if row is None:
|
|
485
|
+
return
|
|
486
|
+
try:
|
|
487
|
+
legacy = json.loads(row["json"])
|
|
488
|
+
except ValueError:
|
|
489
|
+
legacy = {}
|
|
490
|
+
if legacy and legacy.get("app_id") or legacy.get("default_webhook") \
|
|
491
|
+
or legacy.get("base_url") or legacy.get("app_secret") \
|
|
492
|
+
or legacy.get("default_secret"):
|
|
493
|
+
admin = conn.execute(
|
|
494
|
+
"SELECT id FROM users WHERE username='admin'").fetchone()
|
|
495
|
+
if admin is not None and not conn.execute(
|
|
496
|
+
"SELECT 1 FROM feishu_user_cfgs WHERE user_id=?",
|
|
497
|
+
(admin["id"],)).fetchone():
|
|
498
|
+
conn.execute(
|
|
499
|
+
"INSERT INTO feishu_user_cfgs(user_id, json) VALUES(?,?)",
|
|
500
|
+
(admin["id"], json.dumps(legacy, ensure_ascii=False)))
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# 步骤名 → 宿主语言实现(Node 侧 backend/lib/db.js 有同名注册表)
|
|
504
|
+
MIGRATION_HANDLERS = {
|
|
505
|
+
"projects_guide_text_merge": _mh_projects_guide_text_merge,
|
|
506
|
+
"projects_work_dir_from_legacy_cases_root":
|
|
507
|
+
_mh_projects_work_dir_from_legacy_cases_root,
|
|
508
|
+
"feishu_appsettings_bridge": _mh_feishu_appsettings_bridge,
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def migration_when_hit(conn, when):
|
|
513
|
+
"""判定一条声明式迁移的运行条件是否成立(when 为空 = 无条件执行)。
|
|
514
|
+
|
|
515
|
+
守卫 SQL 只读、方言中立(两侧同一份),kind 只有两种:`rows`(有行即跑)与
|
|
516
|
+
`index_origin_unique`(表上存在 UNIQUE 约束索引即跑)。未知 kind 直接报错。
|
|
517
|
+
"""
|
|
518
|
+
if not when:
|
|
519
|
+
return True
|
|
520
|
+
kind = when.get("kind")
|
|
521
|
+
if kind == "rows":
|
|
522
|
+
return conn.execute(when["sql"]).fetchone() is not None
|
|
523
|
+
if kind == "index_origin_unique":
|
|
524
|
+
return any(r["origin"] == "u"
|
|
525
|
+
for r in conn.execute(f"PRAGMA index_list({when['table']})"))
|
|
526
|
+
raise ValueError(f"未知迁移守卫 kind: {kind!r}")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def run_declared_migrations():
|
|
530
|
+
"""执行 MIGRATIONS 清单(migrate() 末尾调用;幂等、可重复跑)。
|
|
531
|
+
|
|
532
|
+
每条一步一个短连接(与 db.py 其余读写同口径),成功即 commit;返回本次**实际
|
|
533
|
+
执行**的步骤 id 列表(跳过的步不计入,供启动日志与测试断言)。未知 handler
|
|
534
|
+
抛 RuntimeError(绝不静默跳过——静默跳过会让两侧库悄悄分叉)。
|
|
535
|
+
"""
|
|
536
|
+
executed = []
|
|
537
|
+
for step in MIGRATIONS:
|
|
538
|
+
conn = connect()
|
|
539
|
+
try:
|
|
540
|
+
if not migration_when_hit(conn, step.get("when")):
|
|
541
|
+
continue
|
|
542
|
+
for stmt in step.get("sql") or ():
|
|
543
|
+
conn.execute(stmt)
|
|
544
|
+
name = step.get("handler")
|
|
545
|
+
if name:
|
|
546
|
+
handler = MIGRATION_HANDLERS.get(name)
|
|
547
|
+
if handler is None:
|
|
548
|
+
raise RuntimeError(f"未知迁移 handler: {name}(步骤 {step['id']})")
|
|
549
|
+
handler(conn)
|
|
550
|
+
conn.commit()
|
|
551
|
+
finally:
|
|
552
|
+
conn.close()
|
|
553
|
+
executed.append(step["id"])
|
|
554
|
+
return executed
|
|
555
|
+
|
|
556
|
+
|
|
271
557
|
def init_db():
|
|
272
558
|
"""建表 + 种子(仅 admin 用户)。返回是否本次新建了默认账号(透传 seed_admin,
|
|
273
559
|
server 启动横幅据此决定是否提示初始口令)。
|
|
@@ -372,86 +658,14 @@ def migrate():
|
|
|
372
658
|
conn.execute("ALTER TABLE projects ADD COLUMN reasoning_effort TEXT NOT NULL DEFAULT ''")
|
|
373
659
|
if "permission_mode" not in pcols:
|
|
374
660
|
conn.execute("ALTER TABLE projects ADD COLUMN permission_mode TEXT NOT NULL DEFAULT ''")
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (r["deploy_spec"] or "").strip():
|
|
384
|
-
extra.append("部署说明:" + r["deploy_spec"].strip())
|
|
385
|
-
if extra:
|
|
386
|
-
merged = (r["guide_text"] or "").rstrip()
|
|
387
|
-
merged = merged + "\n\n" + "\n".join(extra) if merged else "\n".join(extra)
|
|
388
|
-
conn.execute(
|
|
389
|
-
"UPDATE projects SET guide_text=?, commit_spec='', deploy_spec=''"
|
|
390
|
-
" WHERE id=?", (merged, r["id"]))
|
|
391
|
-
conn.execute(
|
|
392
|
-
"UPDATE projects SET user_id="
|
|
393
|
-
" COALESCE((SELECT id FROM users WHERE username='admin'), 0)"
|
|
394
|
-
" WHERE user_id=0")
|
|
395
|
-
# 阻塞让行提交退场(2026-09-13):平台不再自动提交(移交 kimi hook 扩展
|
|
396
|
-
# extensions/kimi-hooks),清理存量卡片上的机器文案。幂等:只命中机器
|
|
397
|
-
# 文案行,清完即空。
|
|
398
|
-
conn.execute(
|
|
399
|
-
"UPDATE board_cards SET block_text=''"
|
|
400
|
-
" WHERE block_text LIKE '提交中:%' OR block_text LIKE '提交失败:%'")
|
|
401
|
-
conn.execute(
|
|
402
|
-
"UPDATE board_cards SET last_error='', last_error_at=NULL"
|
|
403
|
-
" WHERE last_error LIKE '阻塞让行提交失败%'"
|
|
404
|
-
" OR last_error LIKE '阻塞让行提交异常%'")
|
|
405
|
-
# 旧库 projects.name 是全局 UNIQUE;多用户下改为 (user_id, name) 用户内唯一。
|
|
406
|
-
# SQLite 无法直接删除列约束,重建表(重建后同一语句块内建唯一索引)。
|
|
407
|
-
unique_old = [r for r in conn.execute("PRAGMA index_list(projects)")
|
|
408
|
-
if r["origin"] == "u"]
|
|
409
|
-
if unique_old:
|
|
410
|
-
conn.execute("ALTER TABLE projects RENAME TO projects_old")
|
|
411
|
-
conn.executescript("""
|
|
412
|
-
CREATE TABLE projects (
|
|
413
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
414
|
-
user_id INTEGER NOT NULL DEFAULT 0,
|
|
415
|
-
name TEXT NOT NULL,
|
|
416
|
-
project_dir TEXT NOT NULL,
|
|
417
|
-
agent_path TEXT NOT NULL DEFAULT '',
|
|
418
|
-
work_dir TEXT NOT NULL,
|
|
419
|
-
bug_dir TEXT NOT NULL,
|
|
420
|
-
guide_text TEXT NOT NULL DEFAULT '',
|
|
421
|
-
commit_spec TEXT NOT NULL DEFAULT '',
|
|
422
|
-
deploy_spec TEXT NOT NULL DEFAULT '',
|
|
423
|
-
env_label TEXT NOT NULL DEFAULT '',
|
|
424
|
-
created_at TEXT NOT NULL
|
|
425
|
-
);
|
|
426
|
-
INSERT INTO projects(id, user_id, name, project_dir, agent_path, work_dir, bug_dir,
|
|
427
|
-
guide_text, commit_spec, deploy_spec, env_label, created_at)
|
|
428
|
-
SELECT id, user_id, name, project_dir, agent_path, work_dir, bug_dir,
|
|
429
|
-
guide_text, commit_spec, deploy_spec, env_label, created_at
|
|
430
|
-
FROM projects_old;
|
|
431
|
-
DROP TABLE projects_old;
|
|
432
|
-
CREATE UNIQUE INDEX idx_projects_user_name ON projects(user_id, name);
|
|
433
|
-
""")
|
|
434
|
-
else:
|
|
435
|
-
# 新库(或非 UNIQUE 库):补建用户内唯一索引(幂等)
|
|
436
|
-
conn.execute(
|
|
437
|
-
"CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_user_name"
|
|
438
|
-
" ON projects(user_id, name)")
|
|
439
|
-
# 工作目录化迁移(2026-08-30,一次性;幂等标记=bug_dir 非空):
|
|
440
|
-
# 旧版 work_dir 语义为案例库根(项目目录下旧版默认数据目录的 free_style 子目录),
|
|
441
|
-
# 改存工作目录(<项目目录>/.touchstone);bug_dir 列废弃置空(bug 报告目录改由
|
|
442
|
-
# work_dir 派生)。旧 work_dir 以 free_style 结尾则取其父目录,否则回落
|
|
443
|
-
# <项目目录>/.touchstone;磁盘文件一律不动。取父目录分支派生路径与原值一致;
|
|
444
|
-
# 回落分支按新版默认目录(<项目目录>/.touchstone)派生。
|
|
445
|
-
for r in conn.execute(
|
|
446
|
-
"SELECT id, project_dir, work_dir FROM projects"
|
|
447
|
-
" WHERE bug_dir != ''").fetchall():
|
|
448
|
-
old = (r["work_dir"] or "").strip().rstrip("/")
|
|
449
|
-
if old.endswith("/" + CASES_SUBDIR):
|
|
450
|
-
new_w = os.path.dirname(old)
|
|
451
|
-
else:
|
|
452
|
-
new_w = os.path.join(r["project_dir"] or "~", ".touchstone")
|
|
453
|
-
conn.execute("UPDATE projects SET work_dir=?, bug_dir='' WHERE id=?",
|
|
454
|
-
(new_w, r["id"]))
|
|
661
|
+
# 本块原还有四段数据级迁移(guide_text 合并 / user_id 回填 / 卡片文案清理 /
|
|
662
|
+
# 重建表 / 工作目录化)——M1(2026-10-11)起全部移到声明式清单 MIGRATIONS,
|
|
663
|
+
# 由 migrate() 末尾的 run_declared_migrations() 执行(与 Node 侧共用一份
|
|
664
|
+
# backend/schema/migrations.json)。DDL 补列留在原处不动(Node 侧用目标
|
|
665
|
+
# schema 收敛等价覆盖,目标 schema 由 tools/export_schema.py 实测导出)。
|
|
666
|
+
# 旧库 projects.name 全局 UNIQUE 的重建、以及工作目录化迁移,都见 MIGRATIONS
|
|
667
|
+
# 的 2026-09-04_projects_drop_global_unique_name 与
|
|
668
|
+
# 2026-08-30_projects_work_dir_from_legacy_cases_root。
|
|
455
669
|
# board_cards 表补列(旧库升级)
|
|
456
670
|
with connect() as conn:
|
|
457
671
|
cols = {r["name"] for r in conn.execute("PRAGMA table_info(board_cards)")}
|
|
@@ -479,61 +693,27 @@ CREATE UNIQUE INDEX idx_projects_user_name ON projects(user_id, name);
|
|
|
479
693
|
if "unread" not in cols:
|
|
480
694
|
conn.execute("ALTER TABLE board_cards"
|
|
481
695
|
" ADD COLUMN unread INTEGER NOT NULL DEFAULT 0")
|
|
482
|
-
# P4 排队占位单态化迁移(2026-09-19,一次性;幂等:迁移后无 blocked+queue 行):
|
|
483
|
-
# 占位列 2026-09-10 起已落 doing,blocked+queue 仅存量兼容;P4 删除全部双态
|
|
484
|
-
# 判定后该形态无人认领,先归位 doing+queue(磁盘/列语义等价,设计 §5.2)
|
|
485
|
-
with connect() as conn:
|
|
486
|
-
conn.execute("UPDATE board_cards SET column_key='doing'"
|
|
487
|
-
" WHERE block_kind='queue' AND column_key='blocked'")
|
|
488
696
|
# feishu_outbox 补 user_id 列(2026-09-09 起投递归属用户,设置页按用户过滤)
|
|
489
697
|
with connect() as conn:
|
|
490
698
|
cols = {r["name"] for r in conn.execute("PRAGMA table_info(feishu_outbox)")}
|
|
491
699
|
if "user_id" not in cols:
|
|
492
700
|
conn.execute("ALTER TABLE feishu_outbox"
|
|
493
701
|
" ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0")
|
|
494
|
-
#
|
|
495
|
-
# 由
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if admin is not None and not conn.execute(
|
|
510
|
-
"SELECT 1 FROM feishu_user_cfgs WHERE user_id=?",
|
|
511
|
-
(admin["id"],)).fetchone():
|
|
512
|
-
conn.execute(
|
|
513
|
-
"INSERT INTO feishu_user_cfgs(user_id, json) VALUES(?,?)",
|
|
514
|
-
(admin["id"], json.dumps(legacy, ensure_ascii=False)))
|
|
515
|
-
# v3d(2026-09-24):租约层删除——`leases` 表(P5 建、v3a 起只写不读的镜像)
|
|
516
|
-
# 随「占用 / 让行」概念一并退场:占用 = 该单元在「正在开发」队列里有活跃行
|
|
517
|
-
# (wait_items.state),无需第二表征。幂等 DROP:表已不在则零改写;
|
|
518
|
-
# 存量库中的历史行无读口(v3a 起读侧全切行),直接丢弃。
|
|
519
|
-
with connect() as conn:
|
|
520
|
-
conn.execute("DROP TABLE IF EXISTS leases")
|
|
521
|
-
conn.execute("DROP INDEX IF EXISTS idx_leases_project")
|
|
522
|
-
# v2a T1(2026-09-21,裁决 R3):wait_items 状态机扩七枚举,claimed 被
|
|
523
|
-
# starting 吸收。① 存量 claimed 行归一 starting(幂等:改完即无 claimed 行);
|
|
524
|
-
# ② 活跃唯一索引口径扩到四活跃态——旧库索引 WHERE 仍含 'claimed' 时重建
|
|
525
|
-
# (重复启动看索引定义已含 'starting' 即跳过,零改写)。state 列无 CHECK
|
|
526
|
-
# 约束(建表 DDL 复核),无需表重建。
|
|
527
|
-
with connect() as conn:
|
|
528
|
-
conn.execute("UPDATE wait_items SET state='starting' WHERE state='claimed'")
|
|
529
|
-
row = conn.execute(
|
|
530
|
-
"SELECT sql FROM sqlite_master WHERE type='index'"
|
|
531
|
-
" AND name='idx_wait_active'").fetchone()
|
|
532
|
-
if row is not None and "'starting'" not in (row["sql"] or ""):
|
|
533
|
-
conn.execute("DROP INDEX idx_wait_active")
|
|
534
|
-
conn.execute(
|
|
535
|
-
"CREATE UNIQUE INDEX idx_wait_active ON wait_items(kind, target_id)"
|
|
536
|
-
" WHERE state IN ('waiting','starting','running','finishing')")
|
|
702
|
+
# feishu_bindings 补「当前会话」四列(2026-10-08 飞书通用对话:绑定到某个会话)。
|
|
703
|
+
# 旧库的 feishu_bindings 由 CREATE TABLE IF NOT EXISTS 原样保留(不含这四列),
|
|
704
|
+
# 只能在此 ALTER 补齐;幂等:列已存在则跳过,存量行取空默认值(未绑定会话)。
|
|
705
|
+
with connect() as conn:
|
|
706
|
+
cols = {r["name"] for r in conn.execute("PRAGMA table_info(feishu_bindings)")}
|
|
707
|
+
for name, ddl in (("cur_sid", "TEXT NOT NULL DEFAULT ''"),
|
|
708
|
+
("cur_project_id", "INTEGER NOT NULL DEFAULT 0"),
|
|
709
|
+
("cur_title", "TEXT NOT NULL DEFAULT ''"),
|
|
710
|
+
("cur_bound_at", "TEXT NOT NULL DEFAULT ''")):
|
|
711
|
+
if name not in cols:
|
|
712
|
+
conn.execute(f"ALTER TABLE feishu_bindings ADD COLUMN {name} {ddl}")
|
|
713
|
+
# P4 占位单态化(blocked+queue → doing+queue)、旧全局飞书配置桥接、leases 表
|
|
714
|
+
# DROP、claimed 行归一与 idx_wait_active 重建——四段数据级/结构迁移自 M1
|
|
715
|
+
# (2026-10-11)起移入声明式清单 MIGRATIONS(见文件上方),由下方
|
|
716
|
+
# run_declared_migrations() 统一执行。
|
|
537
717
|
# v3c(2026-09-23):证据与心跳迁到行(v3 §2.4,裁决 R2/R11)——wait_items
|
|
538
718
|
# 补 last_seen(心跳时间戳)/evidence(证据 JSON 文本),承载登记证据
|
|
539
719
|
# (desc/reason/pid)与心跳证据(busy=…(poll|sse))。幂等:PRAGMA table_info
|
|
@@ -547,6 +727,9 @@ CREATE UNIQUE INDEX idx_projects_user_name ON projects(user_id, name);
|
|
|
547
727
|
if "evidence" not in wcols:
|
|
548
728
|
conn.execute("ALTER TABLE wait_items ADD COLUMN"
|
|
549
729
|
" evidence TEXT NOT NULL DEFAULT ''")
|
|
730
|
+
# 清单收尾:数据级 / 结构重建迁移(M1 起与 Node 侧共用 backend/schema/
|
|
731
|
+
# migrations.json)。放最后执行——清单里所有步骤都只依赖上面已补齐的列。
|
|
732
|
+
run_declared_migrations()
|
|
550
733
|
|
|
551
734
|
|
|
552
735
|
# 本次进程随机生成的一次性初始口令(仅「未设 TS_ADMIN_PASSWORD 且实际发生种子」时非空),
|
|
@@ -570,13 +753,17 @@ def _ensure_users_columns(conn):
|
|
|
570
753
|
def seed_admin():
|
|
571
754
|
"""无任何用户时插入 admin,返回是否实际创建(bool,供启动横幅决定是否提示)。
|
|
572
755
|
|
|
573
|
-
|
|
756
|
+
口令来源(按优先级):
|
|
574
757
|
- 环境变量 TS_ADMIN_PASSWORD 非空:用之(部署方自定义初始口令,视为有意设定,
|
|
575
758
|
不强制改密)——保持 CI/隔离实例夹具的既有行为;
|
|
576
|
-
-
|
|
759
|
+
- 环境变量 TS_ADMIN_PASSWORDLESS=1(插件形态,薄壳下发,2026-10-08):种**空口令**
|
|
760
|
+
admin、不强制改密。面板本来就是免登 admin(信任头),口令只用于独立形态/直连
|
|
761
|
+
登录;用户之后在设置页设了密码就按设置的来(change_password 存 MD5);
|
|
762
|
+
- 都没有:用 secrets 随机生成 16 位一次性初始口令(URL-safe),并置
|
|
577
763
|
must_change_pw=1,登录后必须先改密才能调用其他 API。
|
|
578
764
|
随机口令经 take_seed_password() 交启动横幅打印一次,不落库明文、不常态打印。
|
|
579
|
-
PBKDF2 哈希 +
|
|
765
|
+
哈希:自定义口令/随机口令沿用 PBKDF2 哈希 + 随机盐(哈希算法为需求约定,勿改);
|
|
766
|
+
空口令走 auth.md5_password("")(与「新写入密码统一 MD5」同口径,且登录时空串即通过)。
|
|
580
767
|
"""
|
|
581
768
|
global _SEED_PASSWORD
|
|
582
769
|
import auth
|
|
@@ -587,10 +774,12 @@ def seed_admin():
|
|
|
587
774
|
_ensure_users_columns(conn)
|
|
588
775
|
env_pw = os.environ.get("TS_ADMIN_PASSWORD")
|
|
589
776
|
if env_pw:
|
|
590
|
-
password, must_change = env_pw, 0
|
|
777
|
+
password, must_change, seed_hash = env_pw, 0, None
|
|
778
|
+
elif os.environ.get("TS_ADMIN_PASSWORDLESS") == "1":
|
|
779
|
+
password, must_change, seed_hash = "", 0, auth.md5_password("")
|
|
591
780
|
else:
|
|
592
|
-
password, must_change = secrets.token_urlsafe(12), 1
|
|
593
|
-
pw_hash, salt = auth.hash_password(password)
|
|
781
|
+
password, must_change, seed_hash = secrets.token_urlsafe(12), 1, None
|
|
782
|
+
pw_hash, salt = seed_hash or auth.hash_password(password)
|
|
594
783
|
conn.execute(
|
|
595
784
|
"INSERT INTO users(username, pass_hash, salt, must_change_pw, created_at)"
|
|
596
785
|
" VALUES(?,?,?,?,?)",
|
|
@@ -1377,13 +1566,18 @@ def get_feishu_binding_by_user(user_id):
|
|
|
1377
1566
|
|
|
1378
1567
|
def set_feishu_binding(open_id, user_id, default_project_id=0):
|
|
1379
1568
|
"""绑定飞书号 ↔ 用户(双向一对一):user_id 已绑其他 open_id 时先清旧绑
|
|
1380
|
-
(换号重绑语义);open_id 重绑直接覆盖(UPDATE)。
|
|
1569
|
+
(换号重绑语义);open_id 重绑直接覆盖(UPDATE)。
|
|
1570
|
+
|
|
1571
|
+
2026-10-08 起「换绑归零」:写入与覆盖都把「当前会话」四列清空——绑定态不能
|
|
1572
|
+
跨身份继承(同一用户换飞书号、同一飞书号换用户,都必须重新显式绑定会话)。"""
|
|
1381
1573
|
with connect() as conn:
|
|
1382
1574
|
conn.execute("DELETE FROM feishu_bindings WHERE user_id=?", (user_id,))
|
|
1383
1575
|
conn.execute(
|
|
1384
|
-
"INSERT INTO feishu_bindings(open_id, user_id, default_project_id
|
|
1385
|
-
"
|
|
1386
|
-
"
|
|
1576
|
+
"INSERT INTO feishu_bindings(open_id, user_id, default_project_id,"
|
|
1577
|
+
" cur_sid, cur_project_id, cur_title, cur_bound_at)"
|
|
1578
|
+
" VALUES(?,?,?,'',0,'','') ON CONFLICT(open_id) DO UPDATE SET"
|
|
1579
|
+
" user_id=excluded.user_id, default_project_id=excluded.default_project_id,"
|
|
1580
|
+
" cur_sid='', cur_project_id=0, cur_title='', cur_bound_at=''",
|
|
1387
1581
|
(open_id, user_id, default_project_id))
|
|
1388
1582
|
|
|
1389
1583
|
|
|
@@ -1405,3 +1599,25 @@ def set_feishu_default_project(open_id, project_id):
|
|
|
1405
1599
|
conn.execute(
|
|
1406
1600
|
"UPDATE feishu_bindings SET default_project_id=? WHERE open_id=?",
|
|
1407
1601
|
(project_id, open_id))
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
def set_feishu_cur_session(open_id, project_id, sid, title=""):
|
|
1605
|
+
"""写「当前会话」绑定(open_id 行必须已存在;时刻取本地时间字符串)。
|
|
1606
|
+
|
|
1607
|
+
当前会话 = 该飞书号私聊普通文本的投递目标:`cur_sid` 空表示未绑定(走默认
|
|
1608
|
+
项目/引导指令),`cur_project_id` 是投递队列的归属项目。sid/title 传空即等价
|
|
1609
|
+
于清空对应字段(None 也按空串落库,避免行上出现 NULL)。
|
|
1610
|
+
"""
|
|
1611
|
+
with connect() as conn:
|
|
1612
|
+
conn.execute(
|
|
1613
|
+
"UPDATE feishu_bindings SET cur_sid=?, cur_project_id=?, cur_title=?,"
|
|
1614
|
+
" cur_bound_at=? WHERE open_id=?",
|
|
1615
|
+
(sid or "", int(project_id or 0), title or "", now_str(), open_id))
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
def clear_feishu_cur_session(open_id):
|
|
1619
|
+
"""清「当前会话」绑定(保留默认项目;未绑定时为 no-op)。"""
|
|
1620
|
+
with connect() as conn:
|
|
1621
|
+
conn.execute(
|
|
1622
|
+
"UPDATE feishu_bindings SET cur_sid='', cur_project_id=0,"
|
|
1623
|
+
" cur_title='', cur_bound_at='' WHERE open_id=?", (open_id,))
|
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
# 面板里就会出现 Touchstone 卡片与该行开关(开关写回 profile patch 的 disabled)。
|
|
9
9
|
#
|
|
10
10
|
# config 约定: repoDir 是**机器相关**路径(Touchstone 平台根,须含 server.py 与 webui/dist-plugin),
|
|
11
|
-
# 不写死在这里 —— 由各 profile 的 cordis.patch.yml 用 `- id: touchstone` + `config:` 覆盖提供
|
|
11
|
+
# 不写死在这里 —— 由各 profile 的 cordis.patch.yml 用 `- id: touchstone-dsh` + `config:` 覆盖提供
|
|
12
12
|
# (覆盖是整键替换,且 profile 层后于 bundle 层应用)。**2026-10-07 起 repoDir 可省**:
|
|
13
13
|
# 包已自包含(Python 平台 + 预构建 webui/dist-plugin 都在包内),薄壳缺省回退到**包自身目录**
|
|
14
14
|
# (lib/index.js 里 PACKAGE_DIR),只有要指向别的检出、或指定 pythonPath 时才写 config。
|
|
15
15
|
# pythonPath 缺省 python3。
|
|
16
16
|
#
|
|
17
17
|
# name 必须是**完整包名**(与仓库根 package.json 的 name、scripts/build.mjs 生成的
|
|
18
|
-
# client.bundle.js 里的模块 id 三者逐字一致);条目 id
|
|
18
|
+
# client.bundle.js 里的模块 id 三者逐字一致);条目 id 是 touchstone-dsh(= 包名去掉 scope)。
|
|
19
|
+
# id 只是组合树里的**本地行键**(profile 覆盖行按它命中、Plugins 面板行开关按它写回),
|
|
20
|
+
# **不是包名**,改它必须与各 profile 的覆盖行同步 —— 否则覆盖落空(repoDir/pythonPath/disabled
|
|
21
|
+
# 全丢,回落包自身目录 + python3),面板开关还会在 profile patch 里追加一行、把旧行留成孤儿。
|
|
22
|
+
# 2026-10-09 由旧名 touchstone 改名(旧名是 2026-10-07 包改名前的残留)。
|
|
19
23
|
- insert:
|
|
20
|
-
- id: touchstone
|
|
24
|
+
- id: touchstone-dsh
|
|
21
25
|
name: "@gavinc-cn/touchstone-dsh"
|