@gavinc-cn/touchstone-dsh 0.1.1 → 0.2.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.
Files changed (56) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +21 -1
  3. package/README.zh.md +11 -1
  4. package/auth.py +0 -0
  5. package/board.py +775 -71
  6. package/builtin_assets.py +0 -0
  7. package/builtin_prompts/free_style/flow.md +0 -0
  8. package/builtin_prompts/free_style/load_test.md +0 -0
  9. package/builtin_prompts/free_style/templates/bug_report_template.md +0 -0
  10. package/builtin_prompts/free_style/templates/case_template.md +0 -0
  11. package/builtin_prompts/free_style/templates/index_template.md +0 -0
  12. package/builtin_prompts/free_style/templates/load_charts_template.md +0 -0
  13. package/builtin_prompts/free_style/templates/load_index_template.md +0 -0
  14. package/builtin_prompts/free_style/templates/load_plan_template.md +0 -0
  15. package/builtin_prompts/free_style/templates/load_scenario_template.md +0 -0
  16. package/builtin_prompts/free_style/templates/status_template.md +0 -0
  17. package/chat.py +590 -17
  18. package/db.py +62 -11
  19. package/dsh-plugin/cordis.patch.yml +7 -3
  20. package/dsh-plugin/install.sh +8 -5
  21. package/dsh-plugin/lib/agent-driver.js +710 -89
  22. package/dsh-plugin/lib/client.bundle.js +94 -9
  23. package/dsh-plugin/lib/index.js +242 -22
  24. package/dsh-plugin/scripts/build.mjs +0 -0
  25. package/dsh-plugin/scripts/prepack-check.mjs +0 -0
  26. package/dsh-plugin/scripts/select-bundle.mjs +0 -0
  27. package/dsh-plugin/src/client.js +94 -9
  28. package/dshdriver.py +74 -5
  29. package/dshevents.py +65 -3
  30. package/export_cases.py +0 -0
  31. package/extensions/dsh-autocommit/asset.json +0 -0
  32. package/extensions/dsh-autocommit/pkg/lib/index.mjs +0 -0
  33. package/extensions/dsh-autocommit/pkg/package.json +0 -0
  34. package/feishu.py +1197 -34
  35. package/feishu_conv.py +920 -0
  36. package/lib.py +0 -0
  37. package/lifecycle.py +0 -0
  38. package/loadcase.py +0 -0
  39. package/loadgen.py +0 -0
  40. package/localbus.py +0 -0
  41. package/migrate_p7b.py +0 -0
  42. package/package.json +1 -1
  43. package/platcompat.py +0 -0
  44. package/prompts.py +0 -0
  45. package/rag.py +0 -0
  46. package/requirements.txt +2 -1
  47. package/server.py +218 -26
  48. package/sessparse.py +150 -44
  49. package/touchstone.py +6 -3
  50. package/waitq.py +23 -0
  51. package/webui/dist-plugin/assets/index-JLwyZZxC.js +484 -0
  52. package/webui/dist-plugin/assets/index-tJ0TlvQ3.css +1 -0
  53. package/webui/dist-plugin/index.html +2 -2
  54. package/worktree.py +0 -0
  55. package/webui/dist-plugin/assets/index-B0WoP1Qy.css +0 -1
  56. package/webui/dist-plugin/assets/index-DtwKoi1T.js +0 -473
package/dshdriver.py CHANGED
@@ -143,9 +143,30 @@ def available(timeout=HEALTH_TIMEOUT):
143
143
  return False
144
144
 
145
145
 
146
+ def live_snapshot(timeout=10):
147
+ """`/live` 的**原始响应**:`{sessions: [...], complete?: bool}`。
148
+
149
+ `complete`(A 批 2026-10-08 起真插件会上报)=插件是否已枚举宿主已有会话:
150
+ 只有它为 true,**空表**才具备「宿主里确实没有会话」的语义(见 dshevents._align)。
151
+ 旧装机副本没有该字段 ⇒ `complete` 缺省按未声明处理(空表=未知)。
152
+
153
+ 为什么单列一个读口(2026-10-08 修正):`live()` 返回的是**行表**(历史调用点
154
+ server.py 直接迭代),而中枢对齐需要「行表 + 完整性声明」两件东西。旧写法拿
155
+ `live()` 的返回值当 dict 取 `.get("sessions")` ⇒ `/live` 非空时必抛
156
+ AttributeError,被 `except` 吞掉后中枢**永久「未对齐」**(`aligned()` 恒 False ⇒
157
+ 看板 recover 不搬列、调和器缺席归位不触发)。契约由
158
+ `tests/test_session_visibility.py::test_live_readers_contract` 钉死。
159
+ """
160
+ return _request("GET", "/live", timeout=timeout) or {}
161
+
162
+
146
163
  def live():
147
- """当前可见会话表(含外部直跑会话):替代逐会话 REST 探测的实时态来源。"""
148
- return _request("GET", "/live", timeout=10).get("sessions", [])
164
+ """当前可见会话表(含外部直跑会话):替代逐会话 REST 探测的实时态来源。
165
+
166
+ 返回**行表 list**(历史契约:server.py 的外部会话汇总直接迭代);需要「完整性
167
+ 声明」的调用方(dshevents._align)用 `live_snapshot()`。
168
+ """
169
+ return list((live_snapshot() or {}).get("sessions") or [])
149
170
 
150
171
 
151
172
  def status(session_id):
@@ -245,9 +266,17 @@ def fork(session_id, at_seq=None):
245
266
  return _request("POST", "/fork", payload, timeout=60)
246
267
 
247
268
 
248
- def rename(session_id, title):
249
- """改会话标题(与 dsh 侧栏同一份标题)。返回 `{title}`。"""
250
- return _request("POST", "/rename", {"session_id": session_id, "title": title})
269
+ def rename(session_id, title, timeout=REQUEST_TIMEOUT):
270
+ """改会话标题(与 dsh 侧栏同一份标题)。返回 `{title, [external]}`。
271
+
272
+ `title` 是**宿主接受值**(可能被 `normalizeSessionTitle` 规范化/按 UTF-8 字节预算
273
+ 截断),调用方据此回写卡面可保证两侧逐字一致(见 board.rename_card_session)。
274
+
275
+ `timeout` 可缩短:卡面改名是 best-effort 的同步链路(`PATCH /board/cards/<cid>`),
276
+ 不能让驱动侧僵死把 HTTP worker 拖满默认 120s。
277
+ """
278
+ return _request("POST", "/rename", {"session_id": session_id, "title": title},
279
+ timeout=timeout)
251
280
 
252
281
 
253
282
  def set_model(session_id, model, provider="", reasoning_effort=""):
@@ -355,6 +384,46 @@ def unarchive(session_id, timeout=30):
355
384
  return archive(session_id, archived=False, timeout=timeout)
356
385
 
357
386
 
387
+ # ---------- 看管声明(外部会话投递通道,C 批 T5 2026-10-10) ----------
388
+
389
+ # 看管声明超时(秒):`/watch` 是驱动侧**纯内存写**,正常毫秒级返回;而投递前置闸
390
+ # (`chat._external_preflight`)是在 HTTP 请求线程上同步调用它的——不能沿用普通请求
391
+ # 的 120s,否则驱动僵死会把用户的一次「发送」卡住两分钟。2s 与「声明不成功即拒投」
392
+ # 的语义相称(设计 §5.1)。
393
+ WATCH_TIMEOUT = 2
394
+
395
+ # 旧插件没有 `/watch` 端点时的状态码(未知驱动端点,见插件 _serve 的兜底 404)。
396
+ # 平台据此把「外部会话投递」降级为拒投并给「插件过旧」文案,池内一切功能不受影响。
397
+ WATCH_UNSUPPORTED = 404
398
+
399
+
400
+ def watch_session(session_id, on=True):
401
+ """声明 / 撤销「平台看管该会话」(驱动 `POST /watch`,幂等)。返回看管是否生效。
402
+
403
+ 为什么需要(C 批设计 §3.1):外部会话(用户在 dsh GUI 里直跑/接管、`/live` 里
404
+ `owned:false`)既不在驱动池、也没有平台持有者,驱动侧因此不可能靠「在不在池内」
405
+ 推断平台是否有意投递——**范围必须由平台显式划定**(宿主里与 Touchstone 无关的
406
+ 会话多得是,无差别放开等于允许平台向任意会话注入消息与答案)。平台只对**平台确实
407
+ 要投递的**会话声明看管:有看板卡的会话由 `board._ensure_watch` 常态声明;任务侧
408
+ 会话在宿主上报为外部态(`owned:false`)时,由消息前置闸 `chat._external_preflight`
409
+ 与作答/审批送达前的 `board._watch_before_delivery` 补声明。驱动据此放行 `/prompt`·
410
+ `/steer`·`/answer`·`/approval` 的池外分支。
411
+
412
+ 幂等:重复声明同一 sid 不报错,驱动回执给的是 `watched` 的**当前实际状态**
413
+ (而非「这次有没有改动」),故本函数返回 `bool(resp["watched"])`——调用方按
414
+ 「True 才算声明成功」处置。`on=False` 撤销(卡删除/归档后的回收,见
415
+ `board._watch_prune`)。
416
+
417
+ 失败语义:**异常照抛**(`DshDriverError`:旧插件 404 / 驱动不可达 / 令牌错),
418
+ 「是否吞异常降级」由调用方决定——`board._ensure_watch` 选择留痕 + 返回 False
419
+ 且不重试(看管失败 ⇒ 投递前置闸拒投,见 `chat._external_preflight`)。
420
+ """
421
+ resp = _request("POST", "/watch",
422
+ {"session_id": session_id, "on": bool(on)},
423
+ timeout=WATCH_TIMEOUT)
424
+ return bool((resp or {}).get("watched"))
425
+
426
+
358
427
  def apply_session_defaults(session_id, model="", provider="",
359
428
  reasoning_effort="", permission_mode="", log=None):
360
429
  """把项目级默认(思考等级 / 权限档)应用到刚建或刚续的会话上(best-effort)。
package/dshevents.py CHANGED
@@ -61,6 +61,11 @@ class EventHub:
61
61
  # 与 `_sessions` 同样遵守「断连=未知」不变量——None 时调用方一律不动作。
62
62
  self._archived = None
63
63
  self._connected = False
64
+ # 快照对齐是否**可信**(2026-10-08 批次,B):只有「(重)连时拿到过权威快照」
65
+ # 才置 True——空快照且插件未声明完整时保留旧表但置 False(见 `_align`)。
66
+ # 读口 `aligned()` = connected ∧ _aligned:调用方据此决定「能否按缺席推断
67
+ # 会话已结束」(`board._iw_once`)与「能否按实况搬列」(`board.recover`)。
68
+ self._aligned = False
64
69
  self._last_seq = 0
65
70
  self._frames = 0
66
71
  self._reconnects = 0
@@ -101,6 +106,22 @@ class EventHub:
101
106
  with self._cond:
102
107
  return self._connected
103
108
 
109
+ def aligned(self):
110
+ """注册表是否**可信**(= 链路在线 ∧ 曾用插件权威快照对齐过)。
111
+
112
+ 为什么需要它(2026-10-08 批次 B,bug_report/20261008_1935):
113
+ 插件重新 apply(热重载)时驱动实例重建、`/live` 会短暂返回空表——旧实现
114
+ `self._sessions = fresh` 直接清空注册表,调用方随即把「未知」当成
115
+ 「会话已结束/空闲」:`board.recover()` 把**正在运行的卡**搬去待审核,
116
+ 且调和器此后再也读不到该会话 ⇒ 不自愈。现在空快照保留旧表并置
117
+ `_aligned=False`,本读口给出「是否可信」的单点判据:
118
+ - False ⇒ 一律不据实况写列(未知 ≠ 空闲,同「断连=未知」不变量);
119
+ - True ⇒ 会话不在注册表即可判「已结束」(`board._iw_once` 的补口)。
120
+ 插件半(A 批次)在 `/live` 里声明 `complete` 后,真·零会话的宿主也能可信。
121
+ """
122
+ with self._cond:
123
+ return bool(self._connected and self._aligned)
124
+
104
125
  def get(self, sid):
105
126
  """单会话实时态;未连接或未知会话返回 None(=未知,不是空闲)。
106
127
 
@@ -146,6 +167,7 @@ class EventHub:
146
167
  """诊断信息(/api 健康与测试断言用)。"""
147
168
  with self._cond:
148
169
  return {"connected": self._connected, "sessions": len(self._sessions),
170
+ "aligned": bool(self._connected and self._aligned),
149
171
  "last_seq": self._last_seq, "frames": self._frames,
150
172
  "reconnects": self._reconnects, "changed": self._changed,
151
173
  "archived": None if self._archived is None else len(self._archived),
@@ -226,8 +248,20 @@ class EventHub:
226
248
  新的状态轮询」。归档集取不到时保持「未知」(None),调用方一律不动作。
227
249
  """
228
250
  try:
229
- rows = (dshdriver.live() or {}).get("sessions") or []
230
- except Exception: # noqa: BLE001 — 对齐失败不阻断重连
251
+ # 用 live_snapshot()(原始 dict)而不是 live()(行表 list)——2026-10-08 修正:
252
+ # 旧写法 `dshdriver.live() or {}` 把行表当 dict 取 `.get("sessions")`,
253
+ # `/live` 非空时必抛 AttributeError 被下面 except 吞掉 ⇒ 中枢**永久未对齐**
254
+ # (aligned() 恒 False,C/D 两处补口在真机等于失效)。契约见
255
+ # tests/test_session_visibility.py::test_live_readers_contract。
256
+ resp = dshdriver.live_snapshot() or {}
257
+ rows = resp.get("sessions") or []
258
+ # 插件完整声明(A 批次后新增,旧插件缺省 None=未声明):只有它才让
259
+ # 「空表」具备「宿主里确实没有会话」的语义,否则空表一律按未知处理。
260
+ complete = resp.get("complete") is True
261
+ except Exception as exc: # noqa: BLE001 — 对齐失败不阻断重连
262
+ self._set_aligned(False) # 取不到快照=不能确认任何事
263
+ print(f"[dshevents] /live 对齐失败({exc}):保留 {len(self._sessions)} 条"
264
+ f"既有会话、标记未对齐", flush=True)
231
265
  return
232
266
  try:
233
267
  archived = {str(s) for s in (dshdriver.archived() or []) if s}
@@ -265,11 +299,34 @@ class EventHub:
265
299
  "inbox": old.get("inbox") or [],
266
300
  "updated_at": now,
267
301
  }
268
- self._sessions = fresh
302
+ # —— 快照可信判定(2026-10-08 批次 B)——
303
+ # 非空快照 = 权威整表;空快照只有插件声明完整(complete)才算权威空。
304
+ # 其余情况(热重载后驱动实例重建、不重枚举已有会话 ⇒ 空表)=**未知**:
305
+ # 旧表原样保留(宁可多等不可误放行),只把可信标志置 False——
306
+ # 调用方一律不据此推断忙/闲,也绝不据「缺席」判会话已结束。
307
+ trusted = bool(fresh) or complete
308
+ if trusted:
309
+ self._sessions = fresh
310
+ kept = len(self._sessions)
311
+ self._aligned = trusted
269
312
  if archived is not None:
270
313
  self._archived = archived
271
314
  self._changed += 1
272
315
  self._cond.notify_all()
316
+ if not trusted and kept:
317
+ # 留痕(插件形态落 <库目录>/plugin-backend.log):这是可诊断的降级态,
318
+ # 不是静默——旧表保留期间看板列不做忙/闲写入,直到下一次权威快照。
319
+ print(f"[dshevents] /live 空快照且插件未声明完整:保留 {kept} 条既有会话、"
320
+ f"标记未对齐(热重载后驱动实例重建的已知形态,见 "
321
+ f"bug_report/20261008_1935)", flush=True)
322
+
323
+ def _set_aligned(self, value):
324
+ """置「快照可信」标志(变更才唤醒订阅者;调用方持锁外语义同 `_set_connected`)。"""
325
+ with self._cond:
326
+ if self._aligned != bool(value):
327
+ self._aligned = bool(value)
328
+ self._changed += 1
329
+ self._cond.notify_all()
273
330
 
274
331
  def _set_connected(self, value):
275
332
  with self._cond:
@@ -401,6 +458,11 @@ def connected():
401
458
  return HUB.connected()
402
459
 
403
460
 
461
+ def aligned():
462
+ """注册表可信读口(在线 ∧ 曾用权威快照对齐);未对齐 ⇒ 调用方不写列、不推断。"""
463
+ return HUB.aligned()
464
+
465
+
404
466
  def get(sid):
405
467
  return HUB.get(sid)
406
468
 
package/export_cases.py CHANGED
File without changes
File without changes
File without changes
File without changes