@gavinc-cn/touchstone-dsh 0.2.0 → 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/LICENSE +0 -0
- package/README.md +23 -15
- package/README.zh.md +17 -12
- package/auth.py +0 -0
- 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 +30 -1
- package/builtin_assets.py +0 -0
- package/builtin_prompts/free_style/flow.md +0 -0
- package/builtin_prompts/free_style/load_test.md +0 -0
- package/builtin_prompts/free_style/templates/bug_report_template.md +0 -0
- package/builtin_prompts/free_style/templates/case_template.md +0 -0
- package/builtin_prompts/free_style/templates/index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_charts_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_plan_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_scenario_template.md +0 -0
- package/builtin_prompts/free_style/templates/status_template.md +0 -0
- package/db.py +294 -129
- package/dsh-plugin/cordis.patch.yml +0 -0
- package/dsh-plugin/install.sh +22 -8
- package/dsh-plugin/lib/client.bundle.js +0 -0
- package/dsh-plugin/lib/index.js +299 -29
- package/dsh-plugin/lib/pack-closure.mjs +291 -0
- package/dsh-plugin/scripts/build.mjs +0 -0
- package/dsh-plugin/scripts/prepack-check.mjs +30 -2
- package/dsh-plugin/scripts/select-bundle.mjs +0 -0
- package/dsh-plugin/src/client.js +0 -0
- package/dshevents.py +0 -0
- package/export_cases.py +0 -0
- package/extensions/dsh-autocommit/asset.json +0 -0
- package/extensions/dsh-autocommit/pkg/lib/index.mjs +0 -0
- package/extensions/dsh-autocommit/pkg/package.json +0 -0
- package/feishu_conv.py +0 -0
- package/lib.py +0 -0
- package/lifecycle.py +116 -13
- package/loadcase.py +0 -0
- package/loadgen.py +0 -0
- package/localbus.py +0 -0
- package/migrate_p7b.py +0 -0
- package/package.json +6 -1
- package/platcompat.py +0 -0
- package/prompts.py +0 -0
- package/rag.py +0 -0
- package/requirements.txt +0 -0
- package/touchstone.py +0 -0
- package/waitq.py +0 -0
- package/webui/dist-plugin/assets/index-ChP3sg6y.css +1 -0
- package/webui/dist-plugin/assets/{index-JLwyZZxC.js → index-UrMyeAXF.js} +110 -110
- package/webui/dist-plugin/index.html +2 -2
- package/worktree.py +0 -0
- package/webui/dist-plugin/assets/index-tJ0TlvQ3.css +0 -1
package/db.py
CHANGED
|
@@ -275,6 +275,285 @@ def now_str():
|
|
|
275
275
|
return time.strftime("%Y-%m-%d %H:%M:%S")
|
|
276
276
|
|
|
277
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
|
+
|
|
278
557
|
def init_db():
|
|
279
558
|
"""建表 + 种子(仅 admin 用户)。返回是否本次新建了默认账号(透传 seed_admin,
|
|
280
559
|
server 启动横幅据此决定是否提示初始口令)。
|
|
@@ -379,86 +658,14 @@ def migrate():
|
|
|
379
658
|
conn.execute("ALTER TABLE projects ADD COLUMN reasoning_effort TEXT NOT NULL DEFAULT ''")
|
|
380
659
|
if "permission_mode" not in pcols:
|
|
381
660
|
conn.execute("ALTER TABLE projects ADD COLUMN permission_mode TEXT NOT NULL DEFAULT ''")
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (r["deploy_spec"] or "").strip():
|
|
391
|
-
extra.append("部署说明:" + r["deploy_spec"].strip())
|
|
392
|
-
if extra:
|
|
393
|
-
merged = (r["guide_text"] or "").rstrip()
|
|
394
|
-
merged = merged + "\n\n" + "\n".join(extra) if merged else "\n".join(extra)
|
|
395
|
-
conn.execute(
|
|
396
|
-
"UPDATE projects SET guide_text=?, commit_spec='', deploy_spec=''"
|
|
397
|
-
" WHERE id=?", (merged, r["id"]))
|
|
398
|
-
conn.execute(
|
|
399
|
-
"UPDATE projects SET user_id="
|
|
400
|
-
" COALESCE((SELECT id FROM users WHERE username='admin'), 0)"
|
|
401
|
-
" WHERE user_id=0")
|
|
402
|
-
# 阻塞让行提交退场(2026-09-13):平台不再自动提交(移交 kimi hook 扩展
|
|
403
|
-
# extensions/kimi-hooks),清理存量卡片上的机器文案。幂等:只命中机器
|
|
404
|
-
# 文案行,清完即空。
|
|
405
|
-
conn.execute(
|
|
406
|
-
"UPDATE board_cards SET block_text=''"
|
|
407
|
-
" WHERE block_text LIKE '提交中:%' OR block_text LIKE '提交失败:%'")
|
|
408
|
-
conn.execute(
|
|
409
|
-
"UPDATE board_cards SET last_error='', last_error_at=NULL"
|
|
410
|
-
" WHERE last_error LIKE '阻塞让行提交失败%'"
|
|
411
|
-
" OR last_error LIKE '阻塞让行提交异常%'")
|
|
412
|
-
# 旧库 projects.name 是全局 UNIQUE;多用户下改为 (user_id, name) 用户内唯一。
|
|
413
|
-
# SQLite 无法直接删除列约束,重建表(重建后同一语句块内建唯一索引)。
|
|
414
|
-
unique_old = [r for r in conn.execute("PRAGMA index_list(projects)")
|
|
415
|
-
if r["origin"] == "u"]
|
|
416
|
-
if unique_old:
|
|
417
|
-
conn.execute("ALTER TABLE projects RENAME TO projects_old")
|
|
418
|
-
conn.executescript("""
|
|
419
|
-
CREATE TABLE projects (
|
|
420
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
421
|
-
user_id INTEGER NOT NULL DEFAULT 0,
|
|
422
|
-
name TEXT NOT NULL,
|
|
423
|
-
project_dir TEXT NOT NULL,
|
|
424
|
-
agent_path TEXT NOT NULL DEFAULT '',
|
|
425
|
-
work_dir TEXT NOT NULL,
|
|
426
|
-
bug_dir TEXT NOT NULL,
|
|
427
|
-
guide_text TEXT NOT NULL DEFAULT '',
|
|
428
|
-
commit_spec TEXT NOT NULL DEFAULT '',
|
|
429
|
-
deploy_spec TEXT NOT NULL DEFAULT '',
|
|
430
|
-
env_label TEXT NOT NULL DEFAULT '',
|
|
431
|
-
created_at TEXT NOT NULL
|
|
432
|
-
);
|
|
433
|
-
INSERT INTO projects(id, user_id, name, project_dir, agent_path, work_dir, bug_dir,
|
|
434
|
-
guide_text, commit_spec, deploy_spec, env_label, created_at)
|
|
435
|
-
SELECT id, user_id, name, project_dir, agent_path, work_dir, bug_dir,
|
|
436
|
-
guide_text, commit_spec, deploy_spec, env_label, created_at
|
|
437
|
-
FROM projects_old;
|
|
438
|
-
DROP TABLE projects_old;
|
|
439
|
-
CREATE UNIQUE INDEX idx_projects_user_name ON projects(user_id, name);
|
|
440
|
-
""")
|
|
441
|
-
else:
|
|
442
|
-
# 新库(或非 UNIQUE 库):补建用户内唯一索引(幂等)
|
|
443
|
-
conn.execute(
|
|
444
|
-
"CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_user_name"
|
|
445
|
-
" ON projects(user_id, name)")
|
|
446
|
-
# 工作目录化迁移(2026-08-30,一次性;幂等标记=bug_dir 非空):
|
|
447
|
-
# 旧版 work_dir 语义为案例库根(项目目录下旧版默认数据目录的 free_style 子目录),
|
|
448
|
-
# 改存工作目录(<项目目录>/.touchstone);bug_dir 列废弃置空(bug 报告目录改由
|
|
449
|
-
# work_dir 派生)。旧 work_dir 以 free_style 结尾则取其父目录,否则回落
|
|
450
|
-
# <项目目录>/.touchstone;磁盘文件一律不动。取父目录分支派生路径与原值一致;
|
|
451
|
-
# 回落分支按新版默认目录(<项目目录>/.touchstone)派生。
|
|
452
|
-
for r in conn.execute(
|
|
453
|
-
"SELECT id, project_dir, work_dir FROM projects"
|
|
454
|
-
" WHERE bug_dir != ''").fetchall():
|
|
455
|
-
old = (r["work_dir"] or "").strip().rstrip("/")
|
|
456
|
-
if old.endswith("/" + CASES_SUBDIR):
|
|
457
|
-
new_w = os.path.dirname(old)
|
|
458
|
-
else:
|
|
459
|
-
new_w = os.path.join(r["project_dir"] or "~", ".touchstone")
|
|
460
|
-
conn.execute("UPDATE projects SET work_dir=?, bug_dir='' WHERE id=?",
|
|
461
|
-
(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。
|
|
462
669
|
# board_cards 表补列(旧库升级)
|
|
463
670
|
with connect() as conn:
|
|
464
671
|
cols = {r["name"] for r in conn.execute("PRAGMA table_info(board_cards)")}
|
|
@@ -486,12 +693,6 @@ CREATE UNIQUE INDEX idx_projects_user_name ON projects(user_id, name);
|
|
|
486
693
|
if "unread" not in cols:
|
|
487
694
|
conn.execute("ALTER TABLE board_cards"
|
|
488
695
|
" ADD COLUMN unread INTEGER NOT NULL DEFAULT 0")
|
|
489
|
-
# P4 排队占位单态化迁移(2026-09-19,一次性;幂等:迁移后无 blocked+queue 行):
|
|
490
|
-
# 占位列 2026-09-10 起已落 doing,blocked+queue 仅存量兼容;P4 删除全部双态
|
|
491
|
-
# 判定后该形态无人认领,先归位 doing+queue(磁盘/列语义等价,设计 §5.2)
|
|
492
|
-
with connect() as conn:
|
|
493
|
-
conn.execute("UPDATE board_cards SET column_key='doing'"
|
|
494
|
-
" WHERE block_kind='queue' AND column_key='blocked'")
|
|
495
696
|
# feishu_outbox 补 user_id 列(2026-09-09 起投递归属用户,设置页按用户过滤)
|
|
496
697
|
with connect() as conn:
|
|
497
698
|
cols = {r["name"] for r in conn.execute("PRAGMA table_info(feishu_outbox)")}
|
|
@@ -509,49 +710,10 @@ CREATE UNIQUE INDEX idx_projects_user_name ON projects(user_id, name);
|
|
|
509
710
|
("cur_bound_at", "TEXT NOT NULL DEFAULT ''")):
|
|
510
711
|
if name not in cols:
|
|
511
712
|
conn.execute(f"ALTER TABLE feishu_bindings ADD COLUMN {name} {ddl}")
|
|
512
|
-
#
|
|
513
|
-
#
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
"SELECT json FROM app_settings WHERE key='feishu'").fetchone()
|
|
517
|
-
if row is not None:
|
|
518
|
-
try:
|
|
519
|
-
legacy = json.loads(row["json"])
|
|
520
|
-
except ValueError:
|
|
521
|
-
legacy = {}
|
|
522
|
-
if legacy and legacy.get("app_id") or legacy.get("default_webhook") \
|
|
523
|
-
or legacy.get("base_url") or legacy.get("app_secret") \
|
|
524
|
-
or legacy.get("default_secret"):
|
|
525
|
-
admin = conn.execute(
|
|
526
|
-
"SELECT id FROM users WHERE username='admin'").fetchone()
|
|
527
|
-
if admin is not None and not conn.execute(
|
|
528
|
-
"SELECT 1 FROM feishu_user_cfgs WHERE user_id=?",
|
|
529
|
-
(admin["id"],)).fetchone():
|
|
530
|
-
conn.execute(
|
|
531
|
-
"INSERT INTO feishu_user_cfgs(user_id, json) VALUES(?,?)",
|
|
532
|
-
(admin["id"], json.dumps(legacy, ensure_ascii=False)))
|
|
533
|
-
# v3d(2026-09-24):租约层删除——`leases` 表(P5 建、v3a 起只写不读的镜像)
|
|
534
|
-
# 随「占用 / 让行」概念一并退场:占用 = 该单元在「正在开发」队列里有活跃行
|
|
535
|
-
# (wait_items.state),无需第二表征。幂等 DROP:表已不在则零改写;
|
|
536
|
-
# 存量库中的历史行无读口(v3a 起读侧全切行),直接丢弃。
|
|
537
|
-
with connect() as conn:
|
|
538
|
-
conn.execute("DROP TABLE IF EXISTS leases")
|
|
539
|
-
conn.execute("DROP INDEX IF EXISTS idx_leases_project")
|
|
540
|
-
# v2a T1(2026-09-21,裁决 R3):wait_items 状态机扩七枚举,claimed 被
|
|
541
|
-
# starting 吸收。① 存量 claimed 行归一 starting(幂等:改完即无 claimed 行);
|
|
542
|
-
# ② 活跃唯一索引口径扩到四活跃态——旧库索引 WHERE 仍含 'claimed' 时重建
|
|
543
|
-
# (重复启动看索引定义已含 'starting' 即跳过,零改写)。state 列无 CHECK
|
|
544
|
-
# 约束(建表 DDL 复核),无需表重建。
|
|
545
|
-
with connect() as conn:
|
|
546
|
-
conn.execute("UPDATE wait_items SET state='starting' WHERE state='claimed'")
|
|
547
|
-
row = conn.execute(
|
|
548
|
-
"SELECT sql FROM sqlite_master WHERE type='index'"
|
|
549
|
-
" AND name='idx_wait_active'").fetchone()
|
|
550
|
-
if row is not None and "'starting'" not in (row["sql"] or ""):
|
|
551
|
-
conn.execute("DROP INDEX idx_wait_active")
|
|
552
|
-
conn.execute(
|
|
553
|
-
"CREATE UNIQUE INDEX idx_wait_active ON wait_items(kind, target_id)"
|
|
554
|
-
" WHERE state IN ('waiting','starting','running','finishing')")
|
|
713
|
+
# P4 占位单态化(blocked+queue → doing+queue)、旧全局飞书配置桥接、leases 表
|
|
714
|
+
# DROP、claimed 行归一与 idx_wait_active 重建——四段数据级/结构迁移自 M1
|
|
715
|
+
# (2026-10-11)起移入声明式清单 MIGRATIONS(见文件上方),由下方
|
|
716
|
+
# run_declared_migrations() 统一执行。
|
|
555
717
|
# v3c(2026-09-23):证据与心跳迁到行(v3 §2.4,裁决 R2/R11)——wait_items
|
|
556
718
|
# 补 last_seen(心跳时间戳)/evidence(证据 JSON 文本),承载登记证据
|
|
557
719
|
# (desc/reason/pid)与心跳证据(busy=…(poll|sse))。幂等:PRAGMA table_info
|
|
@@ -565,6 +727,9 @@ CREATE UNIQUE INDEX idx_projects_user_name ON projects(user_id, name);
|
|
|
565
727
|
if "evidence" not in wcols:
|
|
566
728
|
conn.execute("ALTER TABLE wait_items ADD COLUMN"
|
|
567
729
|
" evidence TEXT NOT NULL DEFAULT ''")
|
|
730
|
+
# 清单收尾:数据级 / 结构重建迁移(M1 起与 Node 侧共用 backend/schema/
|
|
731
|
+
# migrations.json)。放最后执行——清单里所有步骤都只依赖上面已补齐的列。
|
|
732
|
+
run_declared_migrations()
|
|
568
733
|
|
|
569
734
|
|
|
570
735
|
# 本次进程随机生成的一次性初始口令(仅「未设 TS_ADMIN_PASSWORD 且实际发生种子」时非空),
|
|
File without changes
|
package/dsh-plugin/install.sh
CHANGED
|
@@ -50,10 +50,17 @@ pick_python() {
|
|
|
50
50
|
[[ -n "$fallback" ]] && { echo "$fallback"; return 0; }
|
|
51
51
|
return 1
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
# **Python 现在是可选的**(2026-10-15 M8:插件缺省后端由 python 翻成 node):
|
|
54
|
+
# 缺省后端=包内的 `backend/server.js`(要求宿主 Node ≥ 22.19,靠 node:sqlite),
|
|
55
|
+
# 所以「不再要求用户机器上有 Python 与 pip」正是翻缺省的全部收益 —— 装插件不该再被
|
|
56
|
+
# 解释器卡住。解释器只在两种情况下需要:① profile 里显式 `backend: python`(回滚/排障);
|
|
57
|
+
# ② 独立形态(`start.sh` / `touchstone.sh`)仍走 `server.py`。
|
|
58
|
+
# 探不到就只提示一句、**不再**往配置里塞 pythonPath(旧行为是硬报错 exit 1)。
|
|
59
|
+
PYTHON_PATH="$(pick_python || true)"
|
|
60
|
+
if [[ -z "$PYTHON_PATH" ]]; then
|
|
61
|
+
echo "提示: 未找到 Python 解释器(依次试过 python、python3)—— 不影响安装:本插件缺省后端是 Node。"
|
|
62
|
+
echo " 只有要回滚到 Python 后端(profile 里显式写 backend: python)或跑独立形态时才需要它。"
|
|
63
|
+
fi
|
|
57
64
|
|
|
58
65
|
[[ -d "$PROFILE_DIR" ]] || { echo "错误: profile 不存在: $PROFILE_DIR(先用 dsh web 初始化)"; exit 1; }
|
|
59
66
|
if [[ ! -f "$REPO_DIR/webui/dist-plugin/index.html" ]]; then
|
|
@@ -76,7 +83,14 @@ node "$REPO_DIR/dsh-plugin/scripts/select-bundle.mjs" "$PROFILE_DIR/package.json
|
|
|
76
83
|
|
|
77
84
|
echo "完成(包名 $PKG_NAME)。重启 dsh web 生效(package.json 变更 dsh-hmr 也会热重载);"
|
|
78
85
|
echo "之后侧栏 Plugins 面板里会出现 Touchstone 卡片与该行开关。"
|
|
79
|
-
echo "
|
|
80
|
-
echo "
|
|
81
|
-
echo "
|
|
82
|
-
|
|
86
|
+
echo "后端缺省=包内 Node 实现(backend/server.js,要求宿主 Node ≥ 22.19);"
|
|
87
|
+
echo "repoDir 不再必填(缺省 = 包自身目录, 包里自带 Node/Python 平台与预构建前端);"
|
|
88
|
+
echo "只有要指向本机开发检出、或指定解释器(回滚到 Python 后端)时才在 $PROFILE_DIR/cordis.patch.yml 里加:"
|
|
89
|
+
if [[ -n "$PYTHON_PATH" ]]; then
|
|
90
|
+
echo " - id: touchstone-dsh"
|
|
91
|
+
echo " config: { repoDir: $REPO_DIR, pythonPath: $PYTHON_PATH }"
|
|
92
|
+
echo " (要回滚到 Python 后端再加一项 backend: python;注意删掉 backend 键等于 node)"
|
|
93
|
+
else
|
|
94
|
+
echo " - id: touchstone-dsh"
|
|
95
|
+
echo " config: { repoDir: $REPO_DIR } # 本机无 Python:缺省 Node 后端直接可用"
|
|
96
|
+
fi
|
|
File without changes
|