@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/sessparse.py
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
tool_result {call_id, name, text, is_error, truncated}
|
|
20
20
|
(dsh 事件流没有 usage 汇总事件与「本轮失败」独立通道,故不产出 usage / error 条目;
|
|
21
21
|
响应里的 totals 字段保留为 0,维持 API 形状稳定。)
|
|
22
|
+
|
|
23
|
+
上下文占用(会话窗底栏 ctx 圈,2026-10-10 修「恒为 0」):dsh 会话日志里有两个
|
|
24
|
+
可用槽位——`request/context` 事件带**上下文窗口**(适配器声明,见 _parse_dsh)、
|
|
25
|
+
`assistant/message` 带本次调用的 **TokenUsage**;`load()` 在有窗口时附带
|
|
26
|
+
`ctx={"used","max"}`(分子口径=prompt 侧 token 数,见 _dsh_prompt_tokens)。
|
|
22
27
|
"""
|
|
23
28
|
|
|
24
29
|
import base64
|
|
@@ -26,6 +31,7 @@ import glob
|
|
|
26
31
|
import json
|
|
27
32
|
import os
|
|
28
33
|
import re
|
|
34
|
+
import threading
|
|
29
35
|
|
|
30
36
|
import zstandard
|
|
31
37
|
|
|
@@ -70,13 +76,50 @@ def _obj(raw):
|
|
|
70
76
|
return d if isinstance(d, dict) else None
|
|
71
77
|
|
|
72
78
|
|
|
79
|
+
def _nonneg_int(v):
|
|
80
|
+
"""JSON 值 → 非负整数,其他(bool / 字符串 / 负数 / None)一律 None。
|
|
81
|
+
|
|
82
|
+
token 计数与窗口大小都必须是干净的整数:`True` 在 Python 里是 int 子类,
|
|
83
|
+
直接参与算术会算出垃圾值。
|
|
84
|
+
"""
|
|
85
|
+
return v if isinstance(v, int) and not isinstance(v, bool) and v >= 0 else None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _dsh_prompt_tokens(usage):
|
|
89
|
+
"""dsh `TokenUsage` → 本次请求的 **prompt 侧 token 数**(上下文占用分子)。
|
|
90
|
+
|
|
91
|
+
口径照抄 dsh `dsh-token-meter` 的 `pressureFrom`:
|
|
92
|
+
`inputTokens + (cacheReadTokens ?? 0) + (cacheWriteTokens ?? 0)`=「这一次请求
|
|
93
|
+
送进模型的上下文有多大」。**不能**用 totalTokens:它含本次回复的输出 token,
|
|
94
|
+
环会随回复长度越画越大(且与上下文窗口不成比例)。inputTokens 缺失(老格式)
|
|
95
|
+
退回 `totalTokens - outputTokens`;再取不到返回 None(调用方保留上一槽位值)。
|
|
96
|
+
"""
|
|
97
|
+
if not isinstance(usage, dict):
|
|
98
|
+
return None
|
|
99
|
+
inp = _nonneg_int(usage.get("inputTokens"))
|
|
100
|
+
if inp is not None:
|
|
101
|
+
return inp + (_nonneg_int(usage.get("cacheReadTokens")) or 0) \
|
|
102
|
+
+ (_nonneg_int(usage.get("cacheWriteTokens")) or 0)
|
|
103
|
+
total = _nonneg_int(usage.get("totalTokens"))
|
|
104
|
+
out = _nonneg_int(usage.get("outputTokens"))
|
|
105
|
+
if total is not None and out is not None and total >= out:
|
|
106
|
+
return total - out
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
73
110
|
class _Entries:
|
|
74
111
|
"""entry 收集器:自动分配 seq 并累计 totals。"""
|
|
75
112
|
|
|
76
113
|
def __init__(self):
|
|
77
114
|
self.entries = []
|
|
78
|
-
# token/耗时汇总(API 契约字段):dsh 事件流无 usage
|
|
115
|
+
# token/耗时汇总(API 契约字段):dsh 事件流无 usage 汇总事件,恒为 0
|
|
79
116
|
self.totals = {"input": 0, "output": 0, "cache_read": 0, "duration_ms": 0}
|
|
117
|
+
# 上下文占用两槽位(会话窗底栏 ctx 圈,2026-10-10 修「恒为 0」):两个**独立
|
|
118
|
+
# last-wins** 槽位,口径与 dsh token-meter 的 `contextPressure` 投影一致:
|
|
119
|
+
# used = 最新一次 usage 的 prompt 侧 token 数(pressureTokens)
|
|
120
|
+
# max = 最新 `request/context` 事件的 contextWindow(适配器声明的路由窗口)
|
|
121
|
+
# None = 该槽位尚未出现过(老会话 / 适配器未声明窗口 / 还没有一次模型调用)。
|
|
122
|
+
self.ctx = {"used": None, "max": None}
|
|
80
123
|
# dsh 回退锚点(仅 _parse_dsh 填充):[(mid, ese),...] —— 真实用户提问的
|
|
81
124
|
# 合成 mid 与**会话事件 seq**;dsh 没有原地 undo,回退=按该 seq fork 新会话
|
|
82
125
|
self.dsh_anchors = []
|
|
@@ -88,10 +131,30 @@ class _Entries:
|
|
|
88
131
|
|
|
89
132
|
# ---------------------------------------------------------------- dsh 解析
|
|
90
133
|
|
|
91
|
-
# dsh 会话文件为多帧 zstd 拼接(每帧一批 JSONL
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
134
|
+
# dsh 会话文件为多帧 zstd 拼接(每帧一批 JSONL 事件行),逐帧 `decompressobj` 解压
|
|
135
|
+
# (项目唯一第三方依赖,见 requirements.txt)。
|
|
136
|
+
#
|
|
137
|
+
# **解压器必须按线程隔离**(2026-10-09 定因的 SIGSEGV 实障):zstandard 的
|
|
138
|
+
# `ZstdDecompressor` **实例并发调用 `decompressobj()` 不是线程安全的**——原先这里
|
|
139
|
+
# 是模块级共享实例,而站点是多线程的(HTTP 请求线程池 + board 30s 调度线程 +
|
|
140
|
+
# 会话窗读口),并发解压直接 **SIGSEGV 打崩整个后端**(面板 502,薄壳不自愈)。
|
|
141
|
+
# 实测对照(8 线程 × 25s 反复解压真实会话文件):共享实例 **core dumped**;
|
|
142
|
+
# 每线程独立实例同负载 765 次解压**零崩溃**。崩溃现场见
|
|
143
|
+
# `doc_ai/bug_report/20261009_0600_插件后端两次SIGSEGV…`(faulthandler 栈落在
|
|
144
|
+
# `server.py:_api_get_board` 的 `session_title` 调用行,扩展模块清单只有
|
|
145
|
+
# `zstandard.backend_c`)。
|
|
146
|
+
_DEC_TLS = threading.local()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _dec():
|
|
150
|
+
"""取**本线程**的 zstd 解压器(无则新建并缓存,随线程回收)。
|
|
151
|
+
|
|
152
|
+
不要改回模块级共享实例——那正是 SIGSEGV 的根因(见上方注释)。
|
|
153
|
+
"""
|
|
154
|
+
d = getattr(_DEC_TLS, "d", None)
|
|
155
|
+
if d is None:
|
|
156
|
+
d = _DEC_TLS.d = zstandard.ZstdDecompressor()
|
|
157
|
+
return d
|
|
95
158
|
|
|
96
159
|
|
|
97
160
|
def _dsh_decompressed(path):
|
|
@@ -115,7 +178,7 @@ def _dsh_decompressed(path):
|
|
|
115
178
|
chunks = []
|
|
116
179
|
rest = buf
|
|
117
180
|
while rest:
|
|
118
|
-
obj =
|
|
181
|
+
obj = _dec().decompressobj()
|
|
119
182
|
try:
|
|
120
183
|
chunks.append(obj.decompress(rest))
|
|
121
184
|
except zstandard.ZstdError:
|
|
@@ -193,7 +256,7 @@ def _dsh_header(path):
|
|
|
193
256
|
for _ in range(_DSH_HEADER_FRAMES):
|
|
194
257
|
if not rest:
|
|
195
258
|
break
|
|
196
|
-
obj =
|
|
259
|
+
obj = _dec().decompressobj()
|
|
197
260
|
try:
|
|
198
261
|
text = obj.decompress(rest).decode("utf-8", "replace")
|
|
199
262
|
except zstandard.ZstdError:
|
|
@@ -281,8 +344,9 @@ def dsh_latest_session(cwd):
|
|
|
281
344
|
return best
|
|
282
345
|
|
|
283
346
|
|
|
284
|
-
# path -> (stamp, title, first_prompt)
|
|
285
|
-
#
|
|
347
|
+
# path -> (stamp, title, first_prompt, titles):当前标题、首问与全部标题事件同源
|
|
348
|
+
# 同缓存——三者都要全量解压会话文件,一次扫描一起取(30s 同步节拍里同一文件只解压
|
|
349
|
+
# 一次)
|
|
286
350
|
_DSH_TITLE_CACHE = {}
|
|
287
351
|
|
|
288
352
|
|
|
@@ -297,15 +361,31 @@ def _line_at(text, idx):
|
|
|
297
361
|
return text[start:] if end < 0 else text[start:end]
|
|
298
362
|
|
|
299
363
|
|
|
300
|
-
def
|
|
301
|
-
"""
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
364
|
+
def _dsh_scan_titles(text):
|
|
365
|
+
"""全部 `session/title` 事件的标题,按文件顺序(无事件返回 [])。
|
|
366
|
+
|
|
367
|
+
**末枚即 DSH 当前标题**(dsh 对同一会话会写多枚:先 fallback 截断句、再由 LLM
|
|
368
|
+
标题覆盖、用户显式改名再追加;GUI 侧栏显示 last-wins 的那一枚)。平台侧一律
|
|
369
|
+
取同一口径(会话窗 / 任务标题 / 看板 sync 卡标题);`list_sessions` 另把整串
|
|
370
|
+
下发给看板,用于判定「卡面标题是否仍是平台自动写入的形态」。
|
|
371
|
+
|
|
372
|
+
逐次 find + **整行 JSON 校验**(同 `_dsh_scan_prompt`):非事件行里出现同名
|
|
373
|
+
字符串(坏行 / 别处文本)一律跳过;空标题事件跳过(dsh 不会写空标题,真出现
|
|
374
|
+
时按「不表态」处理,避免把当前标题抹成空串)。
|
|
375
|
+
"""
|
|
376
|
+
out = []
|
|
377
|
+
pos = 0
|
|
378
|
+
while True:
|
|
379
|
+
idx = text.find('"session/title"', pos)
|
|
380
|
+
if idx < 0:
|
|
381
|
+
return out
|
|
382
|
+
pos = idx + 1
|
|
383
|
+
d = _obj(_line_at(text, idx))
|
|
384
|
+
if d is None or d.get("type") != "session/title":
|
|
385
|
+
continue
|
|
386
|
+
title = str((d.get("data") or {}).get("title") or "")
|
|
387
|
+
if title:
|
|
388
|
+
out.append(title)
|
|
309
389
|
|
|
310
390
|
|
|
311
391
|
def _dsh_scan_prompt(text):
|
|
@@ -313,7 +393,7 @@ def _dsh_scan_prompt(text):
|
|
|
313
393
|
|
|
314
394
|
判定与会话窗口的 user entry 同口径:`user/message` 且 `source.kind == "user"`
|
|
315
395
|
(系统注入的 plugin / agent-instructions 消息被过滤);文本块按换行拼接、
|
|
316
|
-
|
|
396
|
+
**原样保留内部换行**——调用方(看板同步卡)直接作为卡描述(首问全文)。
|
|
317
397
|
只有图片没有文本的提问返回 `[图片]`(沿用会话窗的图片占位口径)。
|
|
318
398
|
|
|
319
399
|
「第一条」按**文件顺序**取(会话窗的展示顺序;真机 20 个会话核对:文件序首条
|
|
@@ -342,31 +422,33 @@ def _dsh_scan_prompt(text):
|
|
|
342
422
|
|
|
343
423
|
|
|
344
424
|
def _dsh_meta(path):
|
|
345
|
-
"""一次解压取出 (
|
|
346
|
-
|
|
347
|
-
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
425
|
+
"""一次解压取出 (当前标题, 主会话首问原文, 全部标题事件),按文件 stamp 缓存。
|
|
426
|
+
|
|
427
|
+
- 当前标题 = `titles` 的**末枚**(`session/title` 事件 last-wins)= dsh GUI 侧栏
|
|
428
|
+
显示的标题(口径说明见 `_dsh_scan_titles`);
|
|
429
|
+
- 首问 = 见 `_dsh_scan_prompt`,是看板 sync 卡描述(首问全文)的数据源;
|
|
430
|
+
- 全部标题事件(tuple)= 该会话历史上出现过的每个标题,看板 sync 卡据此判定
|
|
431
|
+
「卡面标题是否仍是平台自动写入的形态」(建卡时只有兜底标题、LLM 标题后到的
|
|
432
|
+
窗口里,旧的兜底标题只存在于事件历史中)。
|
|
352
433
|
"""
|
|
353
434
|
try:
|
|
354
435
|
st = os.stat(path)
|
|
355
436
|
stamp = (st.st_mtime_ns, st.st_size)
|
|
356
437
|
except OSError:
|
|
357
|
-
return "", ""
|
|
438
|
+
return "", "", ()
|
|
358
439
|
hit = _DSH_TITLE_CACHE.get(path)
|
|
359
440
|
if hit and hit[0] == stamp:
|
|
360
|
-
return hit[1], hit[2]
|
|
441
|
+
return hit[1], hit[2], hit[3]
|
|
361
442
|
text = _dsh_decompressed(path)
|
|
362
|
-
|
|
443
|
+
titles = tuple(_dsh_scan_titles(text))
|
|
444
|
+
title = titles[-1] if titles else ""
|
|
363
445
|
prompt = _dsh_scan_prompt(text)
|
|
364
|
-
_DSH_TITLE_CACHE[path] = (stamp, title, prompt)
|
|
365
|
-
return title, prompt
|
|
446
|
+
_DSH_TITLE_CACHE[path] = (stamp, title, prompt, titles)
|
|
447
|
+
return title, prompt, titles
|
|
366
448
|
|
|
367
449
|
|
|
368
450
|
def dsh_title(sid):
|
|
369
|
-
"""读取 dsh
|
|
451
|
+
"""读取 dsh 会话当前标题(最后一枚 session/title 事件),无/不可读返回 ''。"""
|
|
370
452
|
path = _dsh_session_file(sid)
|
|
371
453
|
return _dsh_meta(path)[0] if path else ""
|
|
372
454
|
|
|
@@ -374,7 +456,7 @@ def dsh_title(sid):
|
|
|
374
456
|
def dsh_first_prompt(sid):
|
|
375
457
|
"""读取主会话第一条真实用户提问的原文(可多行),无/不可读返回 ''。
|
|
376
458
|
|
|
377
|
-
看板 sync
|
|
459
|
+
看板 sync 卡的描述数据源(见 board.sync_sessions):会话还没收到提问时
|
|
378
460
|
返回 '',调用方按会话标题 / sid 短码兜底,首问落盘后由同步节拍补齐。
|
|
379
461
|
"""
|
|
380
462
|
path = _dsh_session_file(sid)
|
|
@@ -432,6 +514,18 @@ def _parse_dsh(path):
|
|
|
432
514
|
col.add("think", tm, text=b.get("text", ""))
|
|
433
515
|
elif b.get("type") == "text":
|
|
434
516
|
col.add("assistant", tm, text=b.get("text", ""))
|
|
517
|
+
# 上下文占用分子(会话窗 ctx 圈):每次模型调用结算的 prompt 侧 token 数,
|
|
518
|
+
# last-wins(无 usage 的中断消息不参与,槽位保留上一次可用的值)
|
|
519
|
+
used = _dsh_prompt_tokens(data.get("usage"))
|
|
520
|
+
if used is not None:
|
|
521
|
+
col.ctx["used"] = used
|
|
522
|
+
elif t == "request/context":
|
|
523
|
+
# 上下文占用分母(会话窗 ctx 圈):宿主为该路由解析出的上下文窗口
|
|
524
|
+
# (适配器声明才有;中途 /model 换模型会再落一条,故 last-wins)。
|
|
525
|
+
# 0/缺失一律忽略——窗口必须为正才有意义(宿主 schema 也是 positive)。
|
|
526
|
+
win = _nonneg_int(data.get("contextWindow"))
|
|
527
|
+
if win:
|
|
528
|
+
col.ctx["max"] = win
|
|
435
529
|
elif t == "tool/call":
|
|
436
530
|
cid = data.get("callId", "")
|
|
437
531
|
tool_names[cid] = data.get("name", "")
|
|
@@ -548,6 +642,8 @@ def load(family, sid, agent, after=0):
|
|
|
548
642
|
或 {"found": False, "reason": "missing" | "unsupported"}。
|
|
549
643
|
entries 为 seq >= after 的增量;total 为全量条数(变小时客户端应重置重拉)。
|
|
550
644
|
dsh 为单线会话(固定 main,无子 agent)。
|
|
645
|
+
另在**分母(上下文窗口)已知**时附带 `ctx`(会话窗底栏占用圈的数据源,
|
|
646
|
+
见 _dsh_prompt_tokens / `request/context`);窗口未知时**不带**该键。
|
|
551
647
|
"""
|
|
552
648
|
if family not in FAMILIES:
|
|
553
649
|
return {"found": False, "reason": "unsupported"}
|
|
@@ -564,14 +660,21 @@ def load(family, sid, agent, after=0):
|
|
|
564
660
|
cache = _CACHE.get(key)
|
|
565
661
|
if cache is None or cache["stamp"] != stamp:
|
|
566
662
|
col = _parse_dsh(zpath)
|
|
567
|
-
cache = {"stamp": stamp, "entries": col.entries, "totals": col.totals
|
|
663
|
+
cache = {"stamp": stamp, "entries": col.entries, "totals": col.totals,
|
|
664
|
+
"ctx": col.ctx}
|
|
568
665
|
_CACHE[key] = cache
|
|
569
666
|
except (OSError, ValueError):
|
|
570
667
|
return {"found": False, "reason": "missing"}
|
|
571
668
|
entries = cache["entries"]
|
|
572
669
|
after = max(0, int(after or 0))
|
|
573
|
-
|
|
574
|
-
|
|
670
|
+
out = {"found": True, "agents": agents, "agent": agent,
|
|
671
|
+
"entries": entries[after:], "total": len(entries), "totals": cache["totals"]}
|
|
672
|
+
# 上下文占用(会话窗底栏 ctx 圈,2026-10-10):只在**分母已知**时下发——前端按
|
|
673
|
+
# `+ctx.max > 0` 决定画不画进度弧,窗口未知时给了也只能画成 0%(本次修的旧形态)。
|
|
674
|
+
# 分子缺省 0:窗口已知但还没有一次模型调用=空上下文,0% 是真实读数。
|
|
675
|
+
if cache["ctx"]["max"]:
|
|
676
|
+
out["ctx"] = {"used": cache["ctx"]["used"] or 0, "max": cache["ctx"]["max"]}
|
|
677
|
+
return out
|
|
575
678
|
|
|
576
679
|
|
|
577
680
|
def resolve_media(family, sid, agent, media_id):
|
|
@@ -652,7 +755,8 @@ def _dsh_archived_ids():
|
|
|
652
755
|
def session_title(family, sid):
|
|
653
756
|
"""统一会话标题读取(异常静默返回 '',标题只是展示增强,不打断主流程)。
|
|
654
757
|
|
|
655
|
-
dsh
|
|
758
|
+
dsh 标题取会话**当前**标题(末枚 `session/title` 事件,与 dsh GUI 侧栏一致);
|
|
759
|
+
族不在白名单返回 ''。
|
|
656
760
|
"""
|
|
657
761
|
if family not in FAMILIES:
|
|
658
762
|
return ""
|
|
@@ -688,20 +792,22 @@ def _list_dsh(cwd):
|
|
|
688
792
|
sid = os.path.basename(sdir)
|
|
689
793
|
if _header_subagent(_dsh_header(zfile)):
|
|
690
794
|
continue # 子代理会话:先判头行再解全量(省一次整段解压)
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
title, first_prompt = _dsh_meta(zfile)
|
|
694
|
-
out.append({"sid": sid, "title": title, "
|
|
795
|
+
# 一次解压同时取当前标题、首问与全部标题事件(同 stamp 缓存):看板 sync 卡
|
|
796
|
+
# 建卡/标题同步都要后两者,逐会话各调一次读口会重复解压同一个文件(见 _dsh_meta)
|
|
797
|
+
title, first_prompt, titles = _dsh_meta(zfile)
|
|
798
|
+
out.append({"sid": sid, "title": title, "titles": list(titles),
|
|
799
|
+
"first_prompt": first_prompt,
|
|
695
800
|
"mtime": mtime, "archived": sid in archived})
|
|
696
801
|
return out
|
|
697
802
|
|
|
698
803
|
|
|
699
804
|
def list_sessions(family, cwd):
|
|
700
|
-
"""按工作区枚举 dsh 会话 [{sid, title, first_prompt, mtime秒, archived}],
|
|
805
|
+
"""按工作区枚举 dsh 会话 [{sid, title, titles, first_prompt, mtime秒, archived}],
|
|
701
806
|
mtime 降序、上限 LIST_SESSIONS_LIMIT。
|
|
702
807
|
|
|
703
|
-
`title` =
|
|
704
|
-
|
|
808
|
+
`title` = **当前**会话标题(末枚 session/title 事件,与 dsh GUI 一致)、
|
|
809
|
+
`titles` = 全部标题事件、`first_prompt` = 主会话第一条真实用户提问原文
|
|
810
|
+
(看板 sync 卡取它做描述、取 title 做卡标题)。
|
|
705
811
|
|
|
706
812
|
供看板「绑定已有会话」下拉与「自动同步」共用;`archived` 读宿主归档集
|
|
707
813
|
(`<dsh home>/storages/workspace.json` 的 `global.archivedSessionIds`,读不到
|
package/touchstone.py
CHANGED
|
@@ -6,7 +6,7 @@ Windows 原生运行的首选入口(bash/nohup/kill 不可用),Linux 上
|
|
|
6
6
|
.run/server.log)。用法:
|
|
7
7
|
|
|
8
8
|
python3 touchstone.py start|stop|restart|status|build|test|test-full|test-ui
|
|
9
|
-
# Windows: python touchstone.py 或 touchstone.cmd
|
|
9
|
+
# Windows: python touchstone.py 或 touchstone.cmd(后者自动解析 python → python3)
|
|
10
10
|
|
|
11
11
|
行为对齐 touchstone.sh:
|
|
12
12
|
- start / restart 先按需构建前端(npm install 按需:依赖指纹未变化则跳过,
|
|
@@ -117,8 +117,11 @@ def precheck_deps():
|
|
|
117
117
|
import requests # noqa: F401
|
|
118
118
|
import zstandard # noqa: F401
|
|
119
119
|
except ImportError as e:
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
# 用当前解释器自己的路径提示(不写死 python3/python): Windows 官方安装器只有
|
|
121
|
+
# `python.exe`、Linux 惯例是 `python3`, 报用户机器上真正在跑的那个最不容易误导。
|
|
122
|
+
py = sys.executable or "python"
|
|
123
|
+
print_err(f"错误: 当前解释器缺少运行依赖({e}),请先执行:")
|
|
124
|
+
print_err(f" {py} -m pip install -r requirements.txt")
|
|
122
125
|
sys.exit(1)
|
|
123
126
|
|
|
124
127
|
|
package/waitq.py
CHANGED
|
@@ -727,6 +727,9 @@ def selfcheck(probe=None):
|
|
|
727
727
|
的扫描集内**——③ 只取 `kind=KIND_CARD` 的活跃行,ext 行永不被 ③ 覆盖
|
|
728
728
|
(v3 终审修复:原文「③ 只对『卡行已删』形态告警」不成立;③ 的实际报告面
|
|
729
729
|
是两类 c: 行形态——卡不在 doing(列已非开发容器)+ 卡行已删(行无主))。
|
|
730
|
+
反向形态「占位在、行不在」(doing+queue 卡无活跃 c: 行)**不在本函数面**:
|
|
731
|
+
它要读 board_cards 的排队占位语义,归 `board._heal_queue_placeholders`
|
|
732
|
+
的 30s 节拍(报告 + 补建一体,2026-10-10 实障卡 936)。
|
|
730
733
|
probe:行判活探针(web busy 三态;`selfcheck_units` 透传,直调缺省 None)。
|
|
731
734
|
"""
|
|
732
735
|
problems = []
|
|
@@ -1216,6 +1219,26 @@ def msg_get(msg_id):
|
|
|
1216
1219
|
(str(msg_id),)).fetchone()
|
|
1217
1220
|
|
|
1218
1221
|
|
|
1222
|
+
def msg_meta(msg_id):
|
|
1223
|
+
"""消息等待项 meta(取该 target 的**最后一行**,含终态行):飞书回流钩子在
|
|
1224
|
+
终态收口之后读「提交时定格」的载荷(feishu 命名空间)。无行返回 {},坏 JSON 亦 {}。
|
|
1225
|
+
|
|
1226
|
+
只读口:与 msg_get 相邻;口径上不看 state(收口已把行落终态,活跃口径读不到),
|
|
1227
|
+
也不去 join chat_msgs——载荷只在等待项行上(chat_msgs 无 meta 列)。
|
|
1228
|
+
"""
|
|
1229
|
+
with db.connect() as conn:
|
|
1230
|
+
row = conn.execute(
|
|
1231
|
+
"SELECT meta FROM wait_items WHERE kind=? AND target_id=?"
|
|
1232
|
+
" ORDER BY id DESC LIMIT 1", (KIND_MSG, str(msg_id))).fetchone()
|
|
1233
|
+
if row is None:
|
|
1234
|
+
return {}
|
|
1235
|
+
try:
|
|
1236
|
+
meta = json.loads(row["meta"] or "{}")
|
|
1237
|
+
except ValueError:
|
|
1238
|
+
return {}
|
|
1239
|
+
return meta if isinstance(meta, dict) else {}
|
|
1240
|
+
|
|
1241
|
+
|
|
1219
1242
|
def msg_rows(sid=None, card_id=None, states=None):
|
|
1220
1243
|
"""消息行查询(chat 读侧专用):sid/card_id/states 可选过滤,created_at 升序
|
|
1221
1244
|
(idx_chat_msgs_sid 覆盖 sid 过滤);states 传元组。"""
|