@benz-ai-x/dsh-24pa 0.2.0-rc.2.1

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 ADDED
@@ -0,0 +1,153 @@
1
+ # 24私助(24PA)正式插件
2
+
3
+ `@benz-ai-x/dsh-24pa` 是 24私助的正式 dsh bundle:一个「24私助」预设同时承接事务交办、工作区维护与标准模式完整编程能力(F13 起),业务账本使用 PostgreSQL,飞书接入采用官方 SDK 长连接+受控 lark-cli 执行器。
4
+
5
+ 预设携带标准预设的插件全集(persona 已并入助理身份、bash/pwsh、文件与检索、jobs、skill、goal、plan-mode、压缩、subagent/subagent_fork、workflow、ralph、ask-user、todo、web、present)+ pa24 业务插件;各入口实际可见工具由角色白名单决定:本地24私助会话获得标准编程工具全集,飞书接入会话与 Worker 维持业务白名单。`tool-schedule` 不并入——提醒统一经 PostgreSQL 账本。外部 CLI 委派(`subagent_codex`/`subagent_claude_code`)的工具行已启用,但需先向 profile 安装对应 provider 包(`@deepseek-ai/dsh-subagent-codex` / `-claude-code`)并在 AGENTS.md `extraLocalTools` 中显式列出后才对本地会话生效。
6
+
7
+ ## 版本命名
8
+
9
+ 版本号跟随 dsh 基线:**`<dsh 基线版本>.<本产品序号>`**(如 dsh 基线 0.2.0-rc.2 → 本产品 0.2.0-rc.2.1、0.2.0-rc.2.2…)。当前基线=peerDependencies 锁定并经全量验证的 dsh 版本;dsh 基线变化时版本号随之推进并重新核验后发版。
10
+
11
+ ## 双基线支持
12
+
13
+ peerDependencies 同时声明 `0.2.0-rc.2 || 0.2.1-alpha.1`,两基线均通过全量 130 用例:
14
+
15
+ - **dsh 0.2.1-alpha.1**:web 宿主自带 schedule 服务,直接安装即可。
16
+ - **dsh 0.2.0-rc.2**:web 宿主不提供 schedule 服务(官方移入 experimental bundle,且 registry 的 schedule@0.2.0-rc.1 因 peer 不匹配会被禁用)。智能简报/会前准备等依赖原生 Schedule 的功能需另装两件:
17
+ 1. `dsh plugin --profile <p> add @deepseek-ai/dsh-experimental-schedule-bundle`(提供 schedule 服务行)
18
+ 2. `dsh plugin --profile <p> add <rc.2 harness checkout>/packages/schedule/schedule`(registry 版 peer 锁 rc.1 不可用,须目录安装 rc.2 workspace 版)
19
+ 装后宿主的 `schedule_*` 工具对所有会话可见(dsh 会话层注册,预设 restrict 不可隐藏);账本提醒不受影响。
20
+
21
+ ## 安装与启动
22
+
23
+ ```sh
24
+ # 方式一:npm 安装(发布名 @benz-ai-x/dsh-24pa)
25
+ npm install @benz-ai-x/dsh-24pa # 或在 dsh profile 内以包名安装
26
+
27
+ # 方式二:源码安装
28
+ npm install && npm run build # 构建 lib/
29
+ dsh plugin --profile <你的 profile> add <本目录>
30
+ ```
31
+
32
+ > 注:本机默认 registry 若为 npmmirror 镜像,需等待同步或显式 `--registry https://registry.npmjs.org`。
33
+
34
+ 环境变量(值由服务器启动环境提供,文件只保存名称):
35
+
36
+ | 变量 | 用途 |
37
+ |---|---|
38
+ | `PA24_PG_DSN`(默认名,可在 AGENTS.md 改) | PostgreSQL 连接串;业务账本位于独立 `pa24` schema,应使用受限账户 |
39
+ | `PA24_FEISHU_APP_ID` / `PA24_FEISHU_APP_SECRET` | 飞书自建应用凭据(feishu 模式) |
40
+ | `PA24_WORKSPACE` | 首次启动时绑定的工作区绝对目录(也可在面板选择) |
41
+
42
+ 首次绑定目录若没有 `AGENTS.md` 会生成模板;已有文件不覆盖。AGENTS.md 中唯一的 json 块是配置源:`mode`(demo/feishu)、`larkProfile`、`ownerOpenId`、`folderToken`、`tasklistId`、`calendarId`、`timeZone`、`pgDsnEnv`、`maxWorkers`、`enabledWorkers`、`workerModels`、`extraLocalTools`。凭据与连接串只写环境变量名。
43
+
44
+ ## F02 增量(并行事项与记忆维护)
45
+
46
+ - 五类专业角色注册于 `src/roles.ts`:memo 可用;calendar/tasks/reminders/handwriting 已注册、未交付前委派时明确拒绝。维护者可经管理端点注册新职责(如 digest 资料摘要),沿用统一 WorkItem/并发/回传/恢复边界。
47
+ - 飞书回复旧消息固定路由到原事项(`pa24.message_route` 持久路由);未知引用明确告知,不回退新工作。
48
+ - 重启对账:`processing` 收件重派(requestId 幂等)、`admitted` 未交付行提示重发、运行中事项按恢复代次(`work_item.recovery_gen`)续办、已停止事项不复活。
49
+ - JSON 记忆权威:工作区 `.24pa/memory.json`(schemaVersion/revision/records)+审计 `memory-log.jsonl`+可撤销变更集 `.24pa/changesets/`;写入/整理/撤销仅限本地24私助会话(expectedRevision 原子提交),检索对所有角色开放;无任何定时/后台整理。面板「结构化记忆」页展示正文、来源、筛选与分页。
50
+
51
+ ## F03 增量(任务与项目清单)
52
+
53
+ - tasks Worker 正式可用:`task_create`(幂等键+staged 操作,结果未知不自动重试)、`task_update`(截止时间与计划投入/估时分开记录,每任务串行门)、`task_complete`(明确动作,不从对话推断)、`task_get`(尽力远端刷新投影)、`task_list`。
54
+ - 项目/个人清单:`project_create` / `project_adopt`(本人采纳后才创建真实飞书任务并关联)/ `project_progress`(进展取自实际任务状态;不订票、付款或对外提交)。
55
+ - 本地表:`pa24.task`(飞书为权威的投影,含 due/planned/estimate 与同步时间)、`pa24.project`、`pa24.project_task`。平台无任务删除接口:取消以修改+说明处理,如实告知。
56
+
57
+ ## F04 增量(日程与会议安排)
58
+
59
+ - calendar Worker 正式可用:`calendar_query`/`calendar_busy`(先同步授权日历到 `pa24.calendar_event` 投影,报告 syncedAt/窗口/新鲜度与冲突;同步失败返回“投影可能过期”而非“没有会议”)、`calendar_create`/`calendar_update`/`calendar_cancel`(staged 幂等、平台回执、写后回填投影,取消落投影)。
60
+ - `meeting_schedule`:参会人取明确 open_id 或已确认记忆联系人(`联系人:姓名` 主题);同名/缺失必须澄清,未发出任何邀请;邀请仅凭本人明确指令(instruction 依据)。
61
+ - 同步水位在 `pa24.calendar_sync_state`(窗口、complete、last_error);查询窗口上限 62 天。
62
+
63
+ ## F05 增量(个人提醒与免打扰)
64
+
65
+ - reminders Worker 正式可用:`reminder_create`(once afterSeconds/at、every、daily/weekly 本地时间+IANA 时区;时间计算复用 dsh-schedule 公开函数,cron 未支持时如实拒绝)、`reminder_list`、`reminder_cancel/pause/resume`(停止一并取消未发送实例)、`reminder_skip`(跳过本次)、`reminder_snooze`(关联原实例的唯一稍后实例)、`reminder_status`(实例状态+Outbox 平台接受状态,不推断已读)。
66
+ - 机制:规则与发生实例在 `pa24.reminder_rule`/`pa24.reminder_occurrence`(唯一键幂等,SKIP LOCKED 领取);发送经持久 Outbox(模型离线仍能发出);周期规则只补最近一次错过的发生;单 Host 内每 tick 失败不致命。
67
+ - 免打扰/休假(P15/P21):派发时读记忆权威——主题「休假」(confirmed+validUntil)或「通知偏好」(内容含 `安静时段 HH:mm-HH:mm`,按工作区时区);到期实例被扣住(pending+deferred_until)而非丢弃,窗口结束后补发。偏好仅在本地24私助会话经 pa24_memory 维护。
68
+
69
+ ## F06 增量(手写笔记整理与人工审核)
70
+
71
+ - 收集(P28):飞书拍照或发送 JPEG/PNG/WebP 图片文件 → 稳定笔记编号(`pa24.note`/`pa24.note_page`);原稿字节+sha256 存工作区 `.24pa/originals/`,页序与消息来源入账本。回答回执消息继续发图=追加页;不按时间窗自动合并;重复原稿(同 sha)拒绝重复保存;未知格式/超 10 MiB/超 50 页记为缺页并反馈。
72
+ - 识别(P29):handwriting Worker 正式可用,委派必须带 `noteId` 且要求 `workerModels.handwriting` 视觉路由(缺配置明确报错);原稿图片经 dsh 附件服务真实进入子 Agent 与模型请求。Worker 仅提交结构化候选(`note_submit`:转写/摘要/AI建议/疑点/候选行动/相对日期分离),发布为 `docs +create` 模板文档(含【24PA·系统】状态块+原稿图片)+回读全文+指纹(`pa24.note_version` 不可变快照);staged 操作保证重试不重复建文档。本版仅识别单页笔记,多页如实拒绝(F07 交付)。
73
+ - 审核(P31):待审版本经 Outbox 发交互卡片,按钮只携带不透明令牌(`pa24.review_token` 绑定主人/笔记/版本/指纹/7 天有效期);点击时服务端重新核验文档指纹一致才受理,裁决(approve/return)在 PG 事务内落 `pa24.review_decision`+版本/笔记状态,文档状态块随后同步(失败=凭证已保存、同步中)。重复点击返回原结果;Worker 没有任何批准或外部行动工具。
74
+ - 失效与重审(P32):指纹覆盖正文(系统状态块排除在外)、结构顺序与原稿资源摘要;`pa24_notes verify` 有界核验 matches/changed/unknown,changed → 版本转 stale、笔记转 needs_rereview 并通知(旧批准保留为历史凭证);`pa24_notes republish` 以当前文档为 v(n+1) 候选重发审核卡(含行级差异),批准前不继承旧结论。
75
+
76
+ ## F07 增量(多页笔记与集中复核)
77
+
78
+ - 多页识别(P30):单次委派识别整份笔记(≤20 页/批次,超出如实要求分批);`note_submit` 支持 `pages[]` 逐页转写(页号必须精确对应已保存页,缺页如实留空不臆造)、`doubts[]`(数字/人名/缩写/日期/否定/勾选/不清七类重点复核,带 0–1 归一化区域)、`diagrams[]`(保留原图+文字说明,不做矢量重绘)。区域疑点/图示由 sharp 从保真原稿生成 PNG 裁片(`.24pa/crops/`,坐标变换与 certainty 随版本存档),经 `docs +media-insert` 插入文档;回读完整性检查为 图片数 ≥ 页数+裁片数;指纹第三组输入=裁片 sha(页序/疑点坐标/裁片内容任一变化都会换指纹)。单页基线与多页增强共用同一视觉路由与审核管线。
79
+ - 审核队列(P33):`pa24_notes queue` 与面板「手写审核」页集中列出待审/需重审/已退回对象(最新版本、指纹、上次核验结果与时间、页数、在途催办、打开飞书文档);页面只读,无网页批准按钮;空队列/读取失败分别显示。
80
+ - 审核提醒(P33):`pa24_notes remind`(once/daily,绑定当前待审版本)与 `remind_control`(snooze/pause/resume/cancel,只动提醒不动审核);派发复用 Outbox 与 F05 静默扣留,6 小时最小间隔、daily 3 次封顶;版本完成审核或被新候选取代时在途催办同事务自动取消——旧版本不再催审;重启后按账本继续。面板 `notes.poll` / `notes.remind-poll` 暴露有界核对与派发入口。
81
+
82
+ ## F08 增量(事项交办与持续跟进)
83
+
84
+ - 交办与发信(P13):tasks Worker 的 `outreach_send`(`im +messages-send --user-id`,幂等键=操作号)与 `task_assign`(`task +assign`)只凭本人明确指令(instruction 依据);草稿不发送、目标不唯一先澄清(复用 F04 联系人解析);执行回执(平台 message_id/操作号)入 `pa24.outreach`;失败/结果未知如实记录,不伪造成功;等待事项与资料永不构成对外沟通授权。
85
+ - 来源跟随提醒(P19):`reminder_create` 可 `linkTaskGuid`/`linkEventId`(快照来源指纹);提醒引擎**发送前复核**来源仍匹配——取消/改期则拦截实例并告知;任务修改、日程写入与日历同步检测到指纹变化时:未发送实例取消并通知、已发送的补发关联更正(`srccorr`);规则按业务键天然去重,不重写飞书原生重复规则。
86
+ - 周期任务模板(P22):`task_repeat_create`(every≥60s/daily/weekly,dsh-schedule 记录+`pa24.task_template[_instance]` 唯一实例);有界物化 tick 逐实例生成真实飞书任务(幂等键=实例 id);只补最近一次错过(不无界回补);`task_repeat_skip`(跳过本次)/`task_repeat_stop`(停止以后)/`task_repeat_update`(修改以后,重建模板停旧);已生成历史保留。飞书任务 CLI 未暴露原生重复规则,按模板实现(如实说明)。
87
+ - 等待事项(P23):`waiting_create`(内容/来源/检查点/去重键,同源不重复建)+`waiting_control`(收到/继续等/改时间/取消,同步调整检查点);到点经 Outbox **只询问本人**(复用免打扰扣留),不自动催办他人;没有可读回复来源时以本人答复为准。
88
+
89
+ ## F09 增量(每日规划与定期回顾)
90
+
91
+ - 每日规划(P16):calendar Worker `plan_today`(重点/估时/容量/冲突/缺失;confirmed 偏好「会议之间留 N 分钟」计入缓冲;过载如实建议只选重点)、`plan_preview`(明日或含临时插单的候选时间块;插单只列受影响安排,未受影响不动)、`plan_adopt`(只写入选定块,写入前经 staged 日历创建并回执链接)、`overview_today`(今日任务/日历合并视图;未读取范围不显示为零)。建议与写入严格分离,截止与安排时间分开,不自动延期。
92
+ - 智能计划(P24/P25):`pa24.digest_plan[_occurrence]`+原生 ScheduleService(`create/list/delete/history`,仅挂固定飞书接入顶层会话;dsh 原生拒绝 child)。`digest_enable`(morning/evening/weekly 定时、once 演示)/`digest_control`(暂停=移除原生计划保留历史;恢复=重新入队;调整=重建)/`digest_list`。状态三层:Schedule 持久入队(schedule_id)→ 模型工作(occurrence delivering→model_done)→ 平台接受(Outbox sent);简报由 digest Worker `digest_build {planId}` 按窗口唯一键幂等生成(同窗重复投递不重复发布,全部以 planId/scheduleId 归属,不按标题相似盲建)。
93
+ - 投递监督:有界核对对比原生 `schedule.history` 投递与已完成发生——无法归属(宽限后)→ 计划暂停+`digestunconfirmed` 通知,不为过期窗口补发一串;面板 `digest.supervise {graceMs}` 暴露同一入口。重启后原生 Schedule 自持久续跑,业务状态按账本恢复。
94
+ - 简报内容:事实(日程带 syncedAt/新鲜度、任务截止与计划分开、完成、等待、待审、项目进展)与建议分开,每期带来源与数据缺失清单;晚间/周回顾绝不自动延期任务、绝不写 JSON 记忆。
95
+
96
+ ## F10 增量(会议资料与纪要行动)
97
+
98
+ - 会前准备(P26):`meeting_prep_enable {eventId, leadMinutes}` 把一次性原生 Schedule 挂到固定接入会话(schedule_spec 记录会议与提前量);到点 Lead 委派 calendar Worker `meeting_prep_build {planId}`——**发送前复查会议状态**(取消/改期即跳过并说明,不发送过时准备包);资料检索限定授权范围(备忘关键词+近期、confirmed 记忆、相关未完成任务),没有材料如实说明、不编造议程;准备包带资料出处与 WorkItem 标注。日程取消/改期经 F08 来源联动自动停止绑定该会议的 prep 计划。
99
+ - 会后纪要(P27):memo Worker `minutes_build {topic, content, candidates[]}` 经既有 memo 管线保存飞书纪要(创建+回读),候选行动(含出处引文、责任人/日期原话、未知项)存 `pa24.minutes.candidates`——纪要生成≠行动执行。
100
+ - 候选行动契约(可被手写链复用):`minutes_adopt_actions {minutesId, indexes[], instruction}` 只执行选定且信息足够的候选(有 start/end→日历时间块,否则→飞书任务);**幂等键=(minutes, index)**——跨会话重复选择返回已有对象;结果未知拒绝盲重试;部分选择与部分失败逐项返回并回写纪要状态。
101
+
102
+ ## F11 增量(审核笔记的行动与综合查询)
103
+
104
+ - 审核后行动(P34):handwriting 角色 `note_adopt_actions {noteId, versionId, indexes[], instruction}`——使用前核验版本为 approved **且当前文档指纹仍匹配**(未审/已退回/被取代/编辑后/无法核验一律拒绝,未知字段不会自动变成执行参数);候选保留出处(sourceQuote)与日期原话(due 原文+解析值);幂等键=`note.action:<note>:v<版本>:<索引>`(复用 P27 契约,跨会话重复选择返回已有对象);批准与行动授权分别落账。
105
+ - 修订影响(P34):发布新版本时对旧版本已建的行动对象发**变更建议**(`noteimpact` 通知,逐版本去重;"是否调整由你决定",绝不静默删除/覆盖)。
106
+ - 可信检索(P35):`notes_search {query}` 只读检索,逐条**重新核验当前有效性**——matches(可作为已确认事实引用)/changed/unknown(含原因)/unreviewed 明确标注,多版本各自成行、不因曾通过而继承;返回可打开的文档链接。今日概览与晨报新增 reviewedNotes(当前仍匹配的已审版本,带版本/审核时间/链接);待审读取失败显示"无法读取(不按零处理)";会话历史不默认拼接。
107
+
108
+ ## F12 增量(运行维护与数据恢复)
109
+
110
+ - 归档检查/执行(P37):`pa24_maintenance archive_check` 盘点运行中事项、**原生 Schedule**(digest/prep 计划)与 **PG 外部提醒**(reminder_rule/review_reminder,单独展示不与原生计划混同)及未确认发送;`archive_execute` 需 `confirmStop:true`——停止 Worker(用户停止不复活)→ 逐个删除原生计划并回读(部分失败逐项说明)→ 外部提醒默认保留、`stopRules:true` 才停止;恢复不自动重建已删计划;取消零副作用。dsh 原生归档闸(active Schedule 阻止归档)仍是权威兜底。
111
+ - 联合备份(P38):`backup_create {targetDir}` 产出 `pg_dump --schema=pa24`+工作区(AGENTS.md/.24pa 记忆/修订/原稿/裁片)清单+dsh 状态清单+`backup-manifest.json`(各部分 sha256、PG schema 版本与行数水位、凭据**引用名**清单——凭据值永不入备份);`backup_verify` 校验摘要并报告缺失部分(未完整恢复的能力不可假装就绪)。恢复顺序与旧 Outbox/飞书对象对账规则见下方「数据与备份/恢复」。
112
+ - 健康/预算(P39):`pa24_maintenance health`+面板「工作区」页健康区块——能力状态(视觉路由/日历同步新鲜度)、本 Host 投入统计(模型轮次/输入页/裁片/发送队列/结果未知/发送延迟 p50/p95;进程内计数重启清零,费用口径声明)、数据流披露(模型输入范围/日志/保留期/凭据不出现在诊断)、降级语义(模型断→仅模型工作暂停固定提醒继续;PG 断→停止接纳)。
113
+
114
+ ## F13 增量(预设并入标准模式能力)
115
+
116
+ - 预设组装:`cordis.patch.yml` 的 `pa24-preset` 行携带标准预设插件全集(persona 文案并入助理身份)+`pa24-agent`;`tool-schedule` 不并入(提醒统一走账本),subagent 行的 schedule deny 过滤随之移除。
117
+ - 角色白名单(`src/tools.ts` `STANDARD_CODING_TOOLS`):本地24私助会话额外获得 bash/pwsh(按平台择一)、read/write/edit/read_image/glob/grep、job_*、skill、goal、exit_plan_mode、ask_user_question、todo_write、web_fetch/web_search、present、subagent/subagent_fork、interrupt_agent/send_message/list_agents、workflow、ralph;飞书接入会话与 Worker 白名单不变。
118
+ - `extraLocalTools`(封闭枚举 `subagent_codex`/`subagent_claude_code`):安装对应 provider 包后在 AGENTS.md 显式列出,本地会话才获得外部 CLI 委派工具;restrict() 要求名单内工具真实存在,故该能力不能默认开启。
119
+ - 验收:`tests/e2e/f13.e2e.mjs` 经真实 Loader 断言三个角色的实际工具面(本地全集、飞书白名单、Worker 仅 pa24_work+只读记忆)。
120
+
121
+ ## F15 增量(飞书接入配置向导)
122
+
123
+ - `feishu-setup.md` 随插件分发(进入 npm files),是飞书接入配置的唯一权威流程:阶段 0 前置自查 → 1 建自建应用(权限点/长连接事件/发布)→ 2 lark-cli 绑定与用户授权(Device Flow 三步法,`--domain im,task,calendar,docs,drive`)→ 3 资源标识(ownerOpenId/folderToken/tasklistId)→ 4 切换(先 env、后 AGENTS.md、再重启)→ 5 健康检查与 `/24pa` 端到端 → 6 故障对照;附安全红线(密钥不读不写、授权点击与建应用只由本人完成)。
124
+ - `pa24_connection`:`guide` 返回指南全文+插件版本;`check`(`src/feishu.ts` `inspectAccess`)新增 `nextSteps`——把 cli/source/auth(missing/unbound/mismatch/unverified/error)与资源(missing/error)状态映射为带指南锚点的下一步,全部就绪时按 mode 给出切换或端到端验收指引。
125
+ - 系统提示词与 AGENTS.md 模板:配置飞书接入前必读指南、按 check 的 nextSteps 收敛。
126
+ - 测试:`tests/unit/feishu-setup.test.mjs`(指南章节契约+映射全分支)、`tests/e2e/f15.e2e.mjs`(真实 Loader 下 guide/check 经本地会话可达)。
127
+
128
+ ## 数据与备份/恢复
129
+
130
+ | 数据 | 位置 |
131
+ |---|---|
132
+ | 业务账本(收件/事项/操作/Outbox/绑定/备忘/消息路由/任务/日程/提醒/手写笔记与审核凭证) | PostgreSQL `pa24` schema |
133
+ | 长期记忆权威、审计与变更集 | 工作区 `.24pa/`(memory.json / memory-log.jsonl / changesets/) |
134
+ | 手写原稿与疑点裁片(不可变字节与派生副本) | 工作区 `.24pa/originals/`、`.24pa/crops/` |
135
+ | 工作区规则与配置 | 工作区 `AGENTS.md` |
136
+ | 原生会话日志与附件 | dsh `$DSH_HOME/sessions`(原生 JSONL,不使用 SQLite) |
137
+
138
+ 备份:`pg_dump --schema=pa24 <dsn> > pa24.sql` + 工作区目录 + `$DSH_HOME/sessions`。
139
+ 恢复:先恢复 PG(`psql < pa24.sql`)再恢复目录,重启 Host;启动即核对绑定与进行中事项,`needs_reconciliation`/失败事项由人工核对后续办,不自动重放外部写入。
140
+ 单 Host:同一状态目录(`$DSH_HOME/24pa`)第二个活跃进程会被锁拒绝。
141
+
142
+ ## 测试
143
+
144
+ ```sh
145
+ npm run build
146
+ npm test # 单元(真实隔离 PostgreSQL 集群)+ e2e(真实 dsh Loader/Host)
147
+ ```
148
+
149
+ 测试边界(与工单一致):真实 Loader、原生 Session/子代理、隔离 PostgreSQL、真实 lark-cli 子进程(飞书侧为桩);仅替换模型(本地 Messages 兼容 mock)、外部网络与注入时钟。需要相邻的 `deepseek-harness` checkout(`../deepseek-harness`,含已构建 `lib/`)与 Homebrew PostgreSQL(`initdb`/`pg_ctl`)。
150
+
151
+ ## 边界
152
+
153
+ 真实飞书租户联调、真实模型质量与生产部署尚未验收(按工单另记实测/未验证状态)。
package/client.js ADDED
@@ -0,0 +1,379 @@
1
+ // 24私助工作区 panel (F01): overview / Feishu access / workspace tabs with the
2
+ // shared SVG + semantic-color + keyboard/focus vocabulary. Read-mostly; all
3
+ // maintenance happens through the assistant conversation.
4
+ window.__ModuleLoader__.load({
5
+ id: '@benz-ai-x/dsh-24pa',
6
+ factory(require) {
7
+ const React = require('react'), h = React.createElement;
8
+ const zh = {
9
+ panel: '24私助工作区', title: '24私助', work: '事项总览', feishu: '飞书接入', memory: '结构化记忆', workspace: '工作区',
10
+ review: '手写审核', awaiting_review: '待审核', needs_rereview: '需重新审核', returned: '已退回', collecting: '收集中',
11
+ collected: '已收齐', approved: '已通过', pending_review: '待审核', stale: '已失效', superseded: '已取代',
12
+ reminderOnce: '单次', reminderDaily: '每日', reminderPaused: '已暂停', reminderCanceled: '已取消', reminderDone: '已结束',
13
+ queued: '排队中', running: '处理中', completed: '已完成', accepted: '已接纳',
14
+ waiting_input: '等待补充', failed: '未完成', stopped: '已停止', needs_reconciliation: '需核对',
15
+ };
16
+ const paths = {
17
+ memory: ['M7 3h10a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4Z', 'M8 7v10m8-10v10M8 9h4a3 3 0 0 1 0 6H8m8-6h-1'],
18
+ bot: ['M9 4h6M12 4V2', 'M6 7h12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3Z', 'M8 12v2m8-2v2m-7 3h6M1 12v4m22-4v4'],
19
+ chat: ['M21 11.5a8.5 8.5 0 0 1-8.5 8.5H4l-2 2V11.5A8.5 8.5 0 0 1 10.5 3H13a8 8 0 0 1 8 8.5Z', 'M7 10h10M7 14h6'],
20
+ grid: ['M3 3h7v7H3ZM14 3h7v7h-7ZM3 14h7v7H3ZM14 14h7v7h-7Z'],
21
+ note: ['M5 3h10l4 4v14H5ZM14 3v5h5M9 12h6m-6 4h6'],
22
+ tasks: ['M10 6h11M10 12h11M10 18h11', 'm2 5 2 2 3-4m-5 8 2 2 3-4m-5 8 2 2 3-4'],
23
+ plug: ['M8 2v5m8-5v5M6 7h12v4a6 6 0 0 1-12 0ZM12 17v5'],
24
+ pulse: ['M2 12h4l3-8 6 16 3-8h4'],
25
+ refresh: ['M20 7a8 8 0 0 0-14-2L3 8m0-5v5h5M4 17a8 8 0 0 0 14 2l3-3m0 5v-5h-5'],
26
+ check: ['m5 12 4 4L19 6'],
27
+ clock: ['M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM12 7v5l3 2'],
28
+ info: ['M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM12 11v6m0-10v.1'],
29
+ alert: ['m12 3 10 18H2ZM12 9v5m0 3v.1'],
30
+ shield: ['m12 2 8 4v6c0 5-8 10-8 10S4 17 4 12V6Z', 'm8 11 3 3 5-5'],
31
+ code: ['m8 6-6 6 6 6m8-12 6 6-6 6m-3-15-2 18'],
32
+ close: ['m6 6 12 12M6 18 18 6'],
33
+ arrow: ['M4 12h16m-6-6 6 6-6 6'],
34
+ chevron: ['m9 5 7 7-7 7'],
35
+ folder: ['M3 6a2 2 0 0 1 2-2h4l2 3h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z'],
36
+ external: ['M14 3h7v7m0-7-11 11M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5'],
37
+ };
38
+ const icon = (name, props = {}) => h('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, focusable: false, ...props }, (paths[name] || paths.note).map((d, i) => h('path', { d, key: i })));
39
+ const tile = (name, tone = 'neutral') => h('span', { className: 'pa24-icon-tile pa24-tone-' + tone }, icon(name));
40
+ const badge = (label, tone = 'neutral', name) => h('span', { className: 'pa24-badge pa24-tone-' + tone }, name && icon(name, { width: 13, height: 13 }), label);
41
+ const statusStyle = {
42
+ accepted: ['neutral', 'clock'], queued: ['neutral', 'clock'], running: ['blue', 'pulse'], completed: ['blue', 'check'],
43
+ waiting_input: ['amber', 'clock'], failed: ['danger', 'alert'], stopped: ['neutral', 'close'], needs_reconciliation: ['amber', 'info'],
44
+ sent: ['blue', 'check'], pending: ['amber', 'clock'], sending: ['blue', 'pulse'], unknown: ['amber', 'info'], expired: ['neutral', 'close'],
45
+ };
46
+ const toneOf = item => (item.state === 'ok' ? 'blue' : item.state === 'error' ? 'danger' : item.state === 'warn' ? 'amber' : 'neutral');
47
+ const style = `
48
+ .pa24{--pa-ink:var(--dsw-alias-label-primary,#1f2937);--pa-muted:#667085;--pa-line:#e4e7ec;--pa-canvas:#f7f8fa;--pa-surface:var(--dsw-alias-bg-base,#fff);--pa-soft:#f2f4f7;--pa-brand:#2b5da8;--pa-brand-hover:#244f8e;--pa-blue:#2b5da8;--pa-blue-bg:#edf2fa;--pa-amber:#8a5b18;--pa-amber-bg:#fff7e8;--pa-danger:#b42318;--pa-danger-bg:#fff1f0;--pa-shadow:none;height:100%;overflow:auto;container-type:inline-size;padding:32px;color:var(--pa-ink);background:var(--pa-canvas);font:14px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;scrollbar-gutter:stable}
49
+ body[data-ds-dark-theme] .pa24{--pa-muted:#9ba8b9;--pa-line:#303c4a;--pa-canvas:#151b23;--pa-surface:#1d2631;--pa-soft:#25303d;--pa-brand:#3764ab;--pa-brand-hover:#4174c2;--pa-blue:#a0bdf0;--pa-blue-bg:#25364f;--pa-amber:#dfbc83;--pa-amber-bg:#3b3225;--pa-danger:#f0aaa5;--pa-danger-bg:#412b2c;--pa-shadow:none}
50
+ .pa24 *{box-sizing:border-box}.pa24 svg{flex-shrink:0;vertical-align:middle}.pa24 h1,.pa24 h2,.pa24 h3,.pa24 p{margin:0}.pa24 h1{font-size:29px;line-height:1.3;letter-spacing:-.7px;font-weight:700}.pa24 h2{font-size:17px;line-height:1.5;font-weight:650}.pa24 h3{font-size:15px;line-height:1.55;font-weight:650}.pa24 p+p{margin-top:6px}.pa24-wrap{max-width:1100px;margin:auto;min-width:0}
51
+ .pa24-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.pa24-between{justify-content:space-between}.pa24-muted{color:var(--pa-muted)}.pa24-meta{font-size:12px;color:var(--pa-muted);overflow-wrap:anywhere}.pa24-note{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.85}
52
+ .pa24 button,.pa24 .pa24-link-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 14px;border:1px solid var(--pa-line);border-radius:10px;color:var(--pa-ink);background:var(--pa-surface);font:inherit;font-size:13px;font-weight:550;cursor:pointer;transition:background .15s,border-color .15s;text-decoration:none}
53
+ .pa24 button:hover:not(:disabled){background:var(--pa-soft);border-color:var(--pa-muted)}.pa24 button:disabled{opacity:.45;cursor:not-allowed}.pa24 button.pa24-primary{background:var(--pa-blue);color:#fff;border-color:var(--pa-blue)}body[data-ds-dark-theme] .pa24 button.pa24-primary{color:#172c27}
54
+ .pa24 button.pa24-quiet{background:transparent;border-color:transparent;color:var(--pa-muted)}.pa24 button.pa24-quiet:hover:not(:disabled){background:var(--pa-soft);color:var(--pa-ink);border-color:var(--pa-line)}
55
+ .pa24 :is(button,input,select,summary,a,[tabindex]):focus-visible{outline:3px solid var(--pa-blue);outline-offset:3px}.pa24 input,.pa24 select{min-height:42px;width:100%;font:inherit;padding:10px 12px;border:1px solid var(--pa-line);border-radius:10px;background:var(--pa-surface);color:var(--pa-ink)}
56
+ .pa24-icon-tile{width:40px;height:40px;border-radius:12px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;background:var(--tone-bg);color:var(--tone)}.pa24-tone-blue{--tone:var(--pa-blue);--tone-bg:var(--pa-blue-bg)}.pa24-tone-amber{--tone:var(--pa-amber);--tone-bg:var(--pa-amber-bg)}.pa24-tone-danger{--tone:var(--pa-danger);--tone-bg:var(--pa-danger-bg)}.pa24-tone-neutral{--tone:var(--pa-muted);--tone-bg:var(--pa-soft)}.pa24-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;background:var(--tone-bg);color:var(--tone);font-size:11px;font-weight:600;line-height:1.7;white-space:nowrap}
57
+ .pa24-header{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-bottom:24px}.pa24-identity{display:flex;align-items:center;gap:14px}.pa24-brand{width:52px;height:52px;border-radius:12px;background:var(--pa-brand);color:#fff;display:inline-flex;align-items:center;justify-content:center}.pa24-brand svg{width:29px;height:29px}.pa24-eyebrow{font-size:10px;letter-spacing:2px;font-weight:650;color:var(--pa-muted);margin-bottom:3px}.pa24-tagline{margin-top:4px!important;color:var(--pa-muted);font-size:13px}
58
+ .pa24-workspace-strip{display:flex;gap:10px;align-items:center;min-width:0;padding:11px 14px;border:1px solid var(--pa-line);border-radius:12px;background:var(--pa-surface);margin-bottom:18px}.pa24-workspace-strip>svg{color:var(--pa-blue)}.pa24-path{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--pa-muted)}
59
+ .pa24-notice{display:flex;gap:10px;align-items:flex-start;padding:12px 15px;border:1px solid color-mix(in srgb,var(--tone) 16%,transparent);border-radius:12px;color:var(--tone);background:var(--tone-bg);font-size:12px;margin-bottom:20px}.pa24-notice>svg{margin-top:1px}.pa24-notice>div{flex:1;min-width:0}
60
+ .pa24-error{padding:16px;border:1px solid var(--pa-danger);border-radius:12px;background:var(--pa-danger-bg);margin:14px 0;color:var(--pa-danger);overflow-wrap:anywhere}
61
+ .pa24-tabs{display:flex;gap:6px;border-bottom:1px solid var(--pa-line);margin:0 0 25px;overflow-x:auto;scrollbar-width:thin;padding:3px 0 10px}.pa24-tabs button{flex-shrink:0;min-height:40px;padding:8px 12px;background:transparent;border:1px solid transparent;border-radius:9px;color:var(--pa-muted);font-weight:500}.pa24-tabs button[aria-selected=true]{background:var(--tone-bg);color:var(--tone);font-weight:650}
62
+ .pa24-section-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:18px}.pa24-section-title{display:flex;align-items:center;gap:10px}.pa24-section-desc{margin-top:5px!important;color:var(--pa-muted);font-size:13px}
63
+ .pa24-card{min-width:0;background:var(--pa-surface);border:1px solid var(--pa-line);border-radius:16px;padding:22px;margin-bottom:18px;box-shadow:var(--pa-shadow)}.pa24-card>h2{margin-bottom:15px}.pa24-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.pa24-grid>.pa24-card{margin:0}
64
+ .pa24-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:22px}.pa24-stat{display:flex;align-items:center;gap:14px;padding:18px;border:1px solid var(--pa-line);border-radius:14px;background:var(--pa-surface)}.pa24-stat strong{font-size:27px;line-height:1.2;font-weight:650;font-variant-numeric:tabular-nums}.pa24-stat-label{font-size:12px;color:var(--pa-muted);margin-bottom:4px}.pa24-stat small{font-size:11px;color:var(--pa-muted);margin-left:8px}
65
+ .pa24-empty{display:flex;flex-direction:column;align-items:center;text-align:center;padding:35px 18px}.pa24-empty>.pa24-icon-tile{width:64px;height:64px;border-radius:22px;margin-bottom:18px}.pa24-empty>.pa24-icon-tile svg{width:29px;height:29px}.pa24-empty h3{font-size:17px;margin:3px 0 8px}.pa24-empty p{max-width:440px;color:var(--pa-muted);font-size:13px}.pa24-empty .pa24-row{margin-top:18px;justify-content:center}
66
+ .pa24-example{display:flex;align-items:center;gap:9px;padding:12px 16px;background:var(--pa-soft);border-radius:10px;font-size:12px;color:var(--pa-muted)}
67
+ .pa24-dl{margin:0}.pa24-field{display:grid;grid-template-columns:130px minmax(0,1fr);gap:16px;padding:10px 0;border-bottom:1px solid var(--pa-line)}.pa24-field:last-child{border-bottom:0}.pa24-field dt{font-size:12px;color:var(--pa-muted);overflow-wrap:anywhere}.pa24-field dd{margin:0;font-size:13px;overflow-wrap:anywhere}
68
+ .pa24-helper{border-radius:10px;padding:12px 14px;background:var(--pa-soft);color:var(--pa-muted);font-size:12px;margin-top:16px}
69
+ .pa24-searchbar{display:flex;gap:8px;align-items:center;padding:14px;background:var(--pa-surface);border:1px solid var(--pa-line);border-radius:12px;margin-bottom:14px}.pa24-search{position:relative;flex:1;min-width:120px}.pa24-search>svg{position:absolute;left:12px;top:12px;color:var(--pa-muted);width:18px;height:18px}.pa24-search input{padding-left:38px;background:var(--pa-soft);font-size:13px}
70
+ .pa24-result-meta{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 2px 15px;font-size:12px;color:var(--pa-muted)}.pa24-memory-list{display:grid;gap:14px}.pa24-memory-card{background:var(--pa-surface);border:1px solid var(--pa-line);border-radius:14px;overflow:hidden}.pa24-memory-body{padding:20px 22px}.pa24-memory-heading{display:flex;align-items:flex-start;gap:10px;justify-content:space-between}.pa24-memory-heading h3{overflow-wrap:anywhere}.pa24-memory-card .pa24-note{margin:15px 0;font-size:14px}.pa24-memory-source{display:flex;gap:6px;align-items:flex-start;font-size:11px;color:var(--pa-muted);overflow-wrap:anywhere}.pa24-memory-footer{padding:10px 22px;background:var(--pa-soft);border-top:1px solid var(--pa-line)}.pa24-memory-footer details{border:0;margin:0;padding:0}.pa24-memory-footer summary{min-height:26px;padding:0;font-size:11px}.pa24-page-nav{display:flex;align-items:center;justify-content:center;gap:18px;margin-top:22px;font-size:12px;color:var(--pa-muted)}.pa24-page-nav button:first-child svg{transform:rotate(180deg)}
71
+ .pa24-job{padding:18px 0;border-top:1px solid var(--pa-line)}.pa24-job:first-of-type{border-top:0;padding-top:0}.pa24-job .pa24-note{margin:10px 0}.pa24-job .pa24-meta{margin:6px 0}
72
+ .pa24-ready{display:grid;gap:10px}.pa24-ready-item{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid var(--pa-line);border-radius:12px}.pa24-ready-item>svg{margin-top:2px;color:var(--tone)}.pa24-ready-item>div{flex:1;min-width:0}
73
+ .pa24-resource{padding:15px 0;border-bottom:1px solid var(--pa-line)}.pa24-resource:last-child{border:0;padding-bottom:0}.pa24-resource:first-child{padding-top:0}.pa24-resource p{margin-top:5px;font-size:12px;color:var(--pa-muted);overflow-wrap:anywhere}
74
+ .pa24 details{border-top:1px solid var(--pa-line);margin-top:16px;padding-top:10px}.pa24 summary{cursor:pointer;color:var(--pa-muted);font-size:12px;min-height:32px;padding:5px 0}.pa24 pre{padding:14px;border:1px solid var(--pa-line);border-radius:10px;background:var(--pa-soft);white-space:pre-wrap;overflow-wrap:anywhere;max-height:400px;overflow:auto;font:11px/1.75 ui-monospace,SFMono-Regular,Consolas,monospace;margin:10px 0 0}
75
+ .pa24-footer{display:flex;gap:8px;align-items:baseline;margin:26px 0 5px;padding-top:16px;border-top:1px solid var(--pa-line);font-size:11px;color:var(--pa-muted)}
76
+ .pa24-spin{animation:pa24-spin 1s linear infinite}@keyframes pa24-spin{to{transform:rotate(360deg)}}
77
+ @container(max-width:880px){.pa24-header{align-items:flex-start}.pa24-tabs{gap:2px}.pa24-field{grid-template-columns:110px minmax(0,1fr)}}
78
+ @container(max-width:620px){.pa24-header{flex-direction:column;gap:16px}.pa24 h1{font-size:25px}.pa24-grid{grid-template-columns:1fr}.pa24-stats{gap:8px}.pa24-stat{padding:12px;gap:8px}.pa24-stat>.pa24-icon-tile{display:none}.pa24-card{padding:18px}}
79
+ @media(max-width:760px){.pa24{padding:20px 16px}.pa24 button{min-height:44px}.pa24-tabs button{min-height:44px}.pa24 summary{min-height:40px;display:list-item}}
80
+ @media(prefers-reduced-motion:reduce){.pa24 *{animation:none!important;transition:none!important}}
81
+ `;
82
+ return {
83
+ inject: ['slots', 'locale', 'connection', 'uiWorkspace'],
84
+ apply(ctx) {
85
+ ctx.effect(() => ctx.locale.register('pa24', { zh }));
86
+ const t = ctx.locale.bind('pa24');
87
+ const rpc = async (endpoint, payload = {}, signal) => {
88
+ const response = await fetch('./api/24pa', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ endpoint, payload }), signal });
89
+ if (!response.ok) throw new Error('dsh HTTP ' + response.status);
90
+ const result = await response.json();
91
+ if (!result.ok) throw new Error(result.error.message);
92
+ return result.value;
93
+ };
94
+ const date = (value, timeZone) => (value ? new Date(value).toLocaleString('zh-CN', { timeZone: timeZone || 'Asia/Shanghai', hour12: false }) : '尚无记录');
95
+ const badgeOf = status => badge(t(status) || status, ...(statusStyle[status] || ['neutral', 'info']));
96
+ const fields = items => h('dl', { className: 'pa24-dl' }, items.map(([label, value]) => h('div', { key: String(label), className: 'pa24-field' }, h('dt', null, label), h('dd', null, value ?? '未配置'))));
97
+ const sectionHead = (name, title, description, action, tone = 'neutral') => h('div', { className: 'pa24-section-head' }, h('div', null, h('div', { className: 'pa24-section-title' }, h('span', { className: 'pa24-tone-' + tone, style: { color: 'var(--tone)' } }, icon(name)), h('h2', null, title)), description && h('p', { className: 'pa24-section-desc' }, description)), action);
98
+ const empty = (name, tone, title, description, actions) => h('div', { className: 'pa24-empty' }, tile(name, tone), h('h3', null, title), h('p', null, description), actions && h('div', { className: 'pa24-row' }, actions));
99
+
100
+ function MemoryView({ workspace, openRobot }) {
101
+ const [draft, setDraft] = React.useState(''), [query, setQuery] = React.useState(''), [offset, setOffset] = React.useState(0), [retry, setRetry] = React.useState(0);
102
+ const [view, setView] = React.useState({ status: 'loading', data: null, error: '' });
103
+ React.useEffect(() => {
104
+ const abort = new AbortController();
105
+ setView({ status: 'loading', data: null, error: '' });
106
+ void rpc('memory', { query, offset }, abort.signal).then(data => {
107
+ if (!abort.signal.aborted) setView({ status: 'ready', data, error: '' });
108
+ }).catch(error => { if (!abort.signal.aborted) setView({ status: 'error', data: null, error: error.message }); });
109
+ return () => abort.abort();
110
+ }, [workspace.path, query, offset, retry]);
111
+ const categories = { preference: ['个人偏好', 'neutral'], fact: ['事实', 'neutral'], project: ['项目', 'blue'], decision: ['决定', 'neutral'] };
112
+ const search = () => { setOffset(0); setQuery(draft.trim()); setRetry(v => v + 1); };
113
+ const clear = () => { setDraft(''); setQuery(''); setOffset(0); };
114
+ const data = view.data;
115
+ const button = (name, label, onClick, props = {}) => h('button', { type: 'button', onClick, ...props }, icon(name), label);
116
+ return h('section', null,
117
+ sectionHead('memory', '结构化记忆', '记住你的偏好与背景,让每次交办更有默契。',
118
+ h('div', { className: 'pa24-row' }, button('refresh', '刷新记忆', () => setRetry(v => v + 1), { disabled: view.status === 'loading', className: 'pa24-quiet' }), button('chat', '通过对话维护记忆', openRobot)), 'neutral'),
119
+ h('div', { className: 'pa24-searchbar' },
120
+ h('div', { className: 'pa24-search' }, icon('search'), h('input', { value: draft, placeholder: '搜索正文、主题或来源…', 'aria-label': '筛选记忆', onChange: e => setDraft(e.target.value), onKeyDown: e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) search(); } })),
121
+ button('search', '筛选', search), (draft || query) && button('close', '查看全部', clear, { className: 'pa24-quiet' })),
122
+ view.status === 'loading' && h('div', { className: 'pa24-card pa24-row', role: 'status' }, icon('refresh', { className: 'pa24-spin' }), '正在读取记忆内容…'),
123
+ view.status === 'error' && h('div', { role: 'alert', className: 'pa24-error' }, h('div', { className: 'pa24-row' }, icon('alert'), h('strong', null, '记忆读取失败')), h('p', null, view.error), h('p', null, '请在 dsh 的24私助会话中检查记忆文件,修复后重试。'), button('refresh', '重试读取', () => setRetry(v => v + 1))),
124
+ data && h('div', null,
125
+ h('div', { className: 'pa24-result-meta', role: 'status' }, h('span', null, '共 ' + data.total + ' 条记忆' + (query ? ' · 匹配 ' + data.matched + ' 条' : '')), h('span', null, '记忆版本 ' + data.revision)),
126
+ data.total === 0 ? h('div', { className: 'pa24-card' }, empty('memory', 'neutral', '从「记住这件事」开始', '还没有保存的记忆。在 dsh 的24私助会话告诉它你的偏好或项目背景,保存后会直接显示在这里。',
127
+ button('chat', '告诉24私助我的偏好', openRobot, { className: 'pa24-primary' })),
128
+ h('div', { className: 'pa24-example' }, icon('chat', { width: 16, height: 16 }), '试着说:“记住,我希望会议之间留 15 分钟。”')) :
129
+ data.matched === 0 ? h('div', { className: 'pa24-card' }, empty('search', 'neutral', '没有找到匹配的记忆', '试试更短的关键词,或清空筛选查看所有记忆。', button('close', '清空筛选', clear))) :
130
+ h('div', { className: 'pa24-memory-list' }, data.records.map(r => {
131
+ const [category, tone] = categories[r.category] || [r.category, 'neutral'];
132
+ return h('article', { key: r.id, className: 'pa24-memory-card' },
133
+ h('div', { className: 'pa24-memory-body' },
134
+ h('div', { className: 'pa24-memory-heading' }, h('div', { className: 'pa24-row' }, badge(category, tone), h('h3', null, r.topic || '未设置主题')), badge(r.status === 'confirmed' ? '已确认' : '待核实', r.status === 'confirmed' ? 'blue' : 'amber', r.status === 'confirmed' ? 'check' : 'clock')),
135
+ h('div', { className: 'pa24-note' }, r.content),
136
+ h('div', { className: 'pa24-row pa24-between' },
137
+ h('div', { className: 'pa24-memory-source' }, icon('note', { width: 13, height: 13 }), h('span', null, '来源:' + r.source)),
138
+ h('time', { className: 'pa24-meta', dateTime: r.updatedAt }, date(r.updatedAt, workspace.config ? workspace.config.timeZone : 'Asia/Shanghai')))),
139
+ h('div', { className: 'pa24-memory-footer' }, h('details', null, h('summary', null, '修订依据与 JSON'), h('p', { className: 'pa24-meta' }, '修订依据:' + r.reason), h('p', { className: 'pa24-meta' }, '修改会话:' + r.updatedBy), r.validUntil && h('p', { className: 'pa24-meta' }, '有效期至:' + r.validUntil), h('pre', null, JSON.stringify(r, null, 2)))));
140
+ })),
141
+ data.matched > data.limit && h('nav', { className: 'pa24-page-nav', 'aria-label': '记忆分页' }, button('chevron', '上一页', () => setOffset(Math.max(0, data.offset - data.limit)), { disabled: data.offset === 0 }), h('span', null, '第 ' + (Math.floor(data.offset / data.limit) + 1) + ' / ' + Math.ceil(data.matched / data.limit) + ' 页'), button('chevron', '下一页', () => setOffset(data.offset + data.limit), { disabled: data.offset + data.limit >= data.matched }))),
142
+ h('details', null, h('summary', null, '存储位置与维护方式'), h('p', { className: 'pa24-meta' }, (workspace.path || '') + '/.24pa/memory.json'), h('p', { className: 'pa24-meta' }, '通过 dsh 的24私助会话新增、更正、删除和整理(变更集可撤销);整理仅由你发起。')));
143
+ }
144
+
145
+ function ReviewView({ workspace, openRobot }) {
146
+ const [view, setView] = React.useState({ status: 'loading', data: null, error: '' });
147
+ const [retry, setRetry] = React.useState(0);
148
+ React.useEffect(() => {
149
+ const abort = new AbortController();
150
+ setView({ status: 'loading', data: null, error: '' });
151
+ void rpc('notes.queue', {}, abort.signal).then(data => {
152
+ if (!abort.signal.aborted) setView({ status: 'ready', data, error: '' });
153
+ }).catch(error => { if (!abort.signal.aborted) setView({ status: 'error', data: null, error: error.message }); });
154
+ return () => abort.abort();
155
+ }, [workspace.path, retry]);
156
+ const button = (name, label, onClick, props = {}) => h('button', { type: 'button', onClick, ...props }, icon(name), label);
157
+ const tz = workspace.config ? workspace.config.timeZone : 'Asia/Shanghai';
158
+ const verifyBadge = v => {
159
+ if (!v || !v.verifyResult) return badge('尚未核验', 'neutral', 'info');
160
+ if (v.verifyResult === 'matches') return badge('内容一致', 'blue', 'check');
161
+ if (v.verifyResult === 'changed') return badge('文档已修改', 'amber', 'alert');
162
+ return badge('核验异常', 'danger', 'alert');
163
+ };
164
+ return h('section', null,
165
+ sectionHead('pen', '手写审核', '拍照笔记的待审版本、疑点定位与催办都在这里;批准或退回请在飞书审核卡上完成。',
166
+ button('refresh', '刷新队列', () => setRetry(v => v + 1), { disabled: view.status === 'loading', className: 'pa24-quiet' }), 'danger'),
167
+ h('div', { className: 'pa24-example' }, icon('shield', { width: 16, height: 16 }), '审核裁决只由你在飞书卡片上作出;本页面为只读查阅,不提供网页批准按钮。'),
168
+ view.status === 'loading' && h('div', { className: 'pa24-card pa24-row', role: 'status' }, icon('refresh', { className: 'pa24-spin' }), '正在读取审核队列…'),
169
+ view.status === 'error' && h('div', { role: 'alert', className: 'pa24-error' }, h('div', { className: 'pa24-row' }, icon('alert'), h('strong', null, '审核队列读取失败')), h('p', null, view.error), button('refresh', '重试读取', () => setRetry(v => v + 1))),
170
+ view.status === 'ready' && (view.data.count === 0
171
+ ? h('div', { className: 'pa24-card' }, empty('check', 'blue', '没有等待审核的笔记', '拍照整理后的待审版本会出现在这里;审核完成或退回后自动移出。', button('chat', '与24私助对话', openRobot)))
172
+ : view.data.items.map(item => h('article', { key: item.noteId, className: 'pa24-job' },
173
+ h('div', { className: 'pa24-row pa24-between' }, h('h3', null, item.title || item.noteId), badge(t(item.noteStatus) || item.noteStatus, ...(item.noteStatus === 'awaiting_review' ? ['amber', 'clock'] : item.noteStatus === 'needs_rereview' ? ['danger', 'alert'] : ['amber', 'pen']))),
174
+ item.latestVersion && h('div', { className: 'pa24-row' }, badge('v' + item.latestVersion.version + ' ' + (t(item.latestVersion.status) || item.latestVersion.status), item.latestVersion.status === 'pending_review' ? 'amber' : 'neutral', 'shield'), verifyBadge(item.latestVersion)),
175
+ h('p', { className: 'pa24-meta' }, item.pages + ' 页原稿 · 指纹 ' + (item.latestVersion ? item.latestVersion.fingerprint : '—') + (item.latestVersion && item.latestVersion.verifiedAt ? ' · 上次核验 ' + date(item.latestVersion.verifiedAt, tz) : '')),
176
+ item.reminders.length > 0 && h('p', { className: 'pa24-meta' }, '催办:' + item.reminders.map(r => (t('reminder' + (r.kind === 'daily' ? 'Daily' : 'Once')) || r.kind) + ' ' + (t('reminder' + r.status.charAt(0).toUpperCase() + r.status.slice(1)) || r.status) + (r.status === 'pending' ? ',下次 ' + date(r.remindAt, tz) : '')).join(';')),
177
+ item.latestVersion && item.latestVersion.docUrl && h('a', { className: 'pa24-link-button', href: item.latestVersion.docUrl, target: '_blank', rel: 'noreferrer' }, icon('external'), '打开飞书文档'),
178
+ h('p', { className: 'pa24-meta' }, '需要稍后提醒、暂停催办或重新发布候选时,直接告诉24私助。')))));
179
+ }
180
+
181
+ function HealthBlock() {
182
+ const [view, setView] = React.useState({ status: 'idle', data: null, error: '' });
183
+ const load = () => {
184
+ setView({ status: 'loading', data: null, error: '' });
185
+ void rpc('health', {}).then(data => setView({ status: 'ready', data, error: '' })).catch(error => setView({ status: 'error', data: null, error: error.message }));
186
+ };
187
+ const badgeOfCap = c => badge(c.state === 'ok' ? '正常' : c.state === 'warn' ? '需注意' : c.state === 'error' ? '异常' : '说明', c.state === 'ok' ? 'blue' : c.state === 'warn' ? 'amber' : c.state === 'error' ? 'danger' : 'neutral', c.state === 'ok' ? 'check' : c.state === 'warn' ? 'info' : 'alert');
188
+ return h('section', null,
189
+ sectionHead('pulse', '健康与预算', '能力可用性、同步新鲜度与本 Host 的投入统计;凭据不进入诊断。',
190
+ h('div', { className: 'pa24-row' }, button('refresh', view.status === 'idle' ? '读取健康状态' : '刷新', load, { disabled: view.status === 'loading', className: view.status === 'idle' ? 'pa24-primary' : 'pa24-quiet' })), 'blue'),
191
+ view.status === 'loading' && h('div', { className: 'pa24-card pa24-row', role: 'status' }, icon('refresh', { className: 'pa24-spin' }), '正在读取健康状态…'),
192
+ view.status === 'error' && h('div', { role: 'alert', className: 'pa24-error' }, h('div', { className: 'pa24-row' }, icon('alert'), h('strong', null, '健康状态读取失败')), h('p', null, view.error), button('refresh', '重试', load)),
193
+ view.status === 'ready' && h('div', { className: 'pa24-card' },
194
+ h('div', { className: 'pa24-ready' }, (view.data.capabilities ?? []).map(c => h('div', { key: c.id, className: 'pa24-ready-item pa24-tone-' + (c.state === 'ok' ? 'blue' : c.state === 'warn' ? 'amber' : 'danger') }, icon(c.state === 'ok' ? 'check' : 'alert', { width: 17, height: 17 }), h('div', null, h('div', { className: 'pa24-row pa24-between' }, h('strong', null, c.id), badgeOfCap(c)), h('p', { className: 'pa24-meta' }, c.message))))),
195
+ view.data.usage && h('div', { style: { marginTop: 14 } }, fields([
196
+ ['模型轮次', view.data.usage.modelTurns], ['输入笔记页', view.data.usage.notePagesInput], ['生成裁片', view.data.usage.noteCrops],
197
+ ['发送队列', view.data.usage.outboxQueued], ['结果未知', (view.data.usage.outboxUnknown ?? 0) + (view.data.usage.operationsUnknown ?? 0)],
198
+ ['发送延迟 p50/p95', (view.data.usage.outboxLatencyMs?.p50 ?? '—') + ' / ' + (view.data.usage.outboxLatencyMs?.p95 ?? '—') + ' ms'],
199
+ ]), h('p', { className: 'pa24-meta' }, view.data.usage.note)),
200
+ view.data.dataFlow && h('details', null, h('summary', null, '数据流与保留披露'), fields([
201
+ ['模型输入', view.data.dataFlow.modelInput], ['额外日志', view.data.dataFlow.extraLogs], ['保留期限', view.data.dataFlow.retention], ['凭据', view.data.dataFlow.secrets],
202
+ ])),
203
+ view.data.degraded && h('div', { className: 'pa24-example' }, icon('info', { width: 16, height: 16 }), view.data.degraded.modelDown + ' ' + view.data.degraded.pgDown)));
204
+ }
205
+
206
+ function Panel() {
207
+ const [state, setState] = React.useState(null), [error, setError] = React.useState(''), [connError, setConnError] = React.useState(''), [busy, setBusy] = React.useState(false);
208
+ const [tab, setTab] = React.useState('work'), [path, setPath] = React.useState('');
209
+ const main = React.useRef(null);
210
+ const refresh = async () => { const s = await rpc('snapshot'); setState(s); setConnError(''); return s; };
211
+ React.useEffect(() => {
212
+ let live = true, pending = false;
213
+ const load = async () => {
214
+ if (pending) return;
215
+ pending = true;
216
+ try { const s = await rpc('snapshot'); if (live) { setState(s); setConnError(''); } }
217
+ catch (e) { if (live) setConnError(e.message); }
218
+ finally { pending = false; }
219
+ };
220
+ void load(); const timer = setInterval(() => void load(), 2500);
221
+ return () => { live = false; clearInterval(timer); };
222
+ }, []);
223
+ const run = async fn => { setBusy(true); setError(''); try { return await fn(); } catch (e) { setError(e.message); return null; } finally { setBusy(false); } };
224
+ const button = (name, label, fn, props = {}) => h('button', { type: 'button', disabled: busy, onClick: () => void run(fn), ...props }, name && icon(name), label);
225
+ const selectTab = id => { setTab(id); main.current?.scrollTo({ top: 0 }); };
226
+ const openRobot = async () => { const r = await rpc('action', { type: 'robot.open' }); ctx.uiWorkspace.openSession(r.sessionId); };
227
+ if (!state) return h('main', { className: 'pa24' }, h('style', null, style), h('div', { className: 'pa24-wrap' }, empty(connError ? 'alert' : 'bot', connError ? 'danger' : 'blue', connError ? '暂时无法连接工作区' : '正在连接你的工作区', connError || '正在读取配置与当前事项…')));
228
+ const workspace = state.workspace, config = workspace?.config, tz = config?.timeZone;
229
+ const items = state.work || [];
230
+ const active = items.filter(j => ['accepted', 'queued', 'running'].includes(j.status));
231
+ const tabItems = [['work', 'grid', 'blue'], ['feishu', 'plug', 'blue'], ['memory', 'memory', 'blue'], ['review', 'pen', 'blue'], ['workspace', 'folder', 'blue']];
232
+ let content;
233
+ if (tab === 'review') {
234
+ content = h(ReviewView, { workspace, openRobot });
235
+ } else if (tab === 'work') {
236
+ const roleNames = { memo: '备忘整理' };
237
+ const readyItems = (state.readiness?.items || []).map(item => {
238
+ const labels = { host: '宿主', config: '配置', postgres: 'PostgreSQL', workspace: '工作区', feishu: '飞书接入', sessions: '固定会话' };
239
+ const stateLabel = item.state === 'ok' ? '正常' : item.state === 'error' ? '需要处理' : '待核验';
240
+ const iconName = item.state === 'ok' ? 'check' : item.state === 'error' ? 'alert' : 'clock';
241
+ return h('div', { key: item.id, className: 'pa24-ready-item pa24-tone-' + toneOf(item) },
242
+ icon(iconName, { width: 17, height: 17 }),
243
+ h('div', null,
244
+ h('div', { className: 'pa24-row pa24-between' }, h('strong', null, labels[item.id] || item.id), badge(stateLabel, toneOf(item))),
245
+ h('p', { className: 'pa24-meta' }, item.message)));
246
+ });
247
+ const stats = [
248
+ ['pulse', 'blue', '正在处理', active.length, '含排队事项'],
249
+ ['check', 'blue', '已完成', items.filter(j => j.status === 'completed').length, '历史事项'],
250
+ ['note', 'neutral', '备忘已保存', (state.memos ?? items.filter(j => j.role === 'memo' && j.status === 'completed')).length, '随手记与资料'],
251
+ ];
252
+ const jobCards = items.length
253
+ ? items.map(j => h('article', { className: 'pa24-job', key: j.id },
254
+ h('div', { className: 'pa24-row pa24-between' }, h('h3', null, j.title), badgeOf(j.status)),
255
+ h('p', { className: 'pa24-meta' }, (j.origin === 'local' ? 'dsh 会话' : '飞书') + ' · ' + (roleNames[j.role] || j.role)),
256
+ j.progress && h('p', { className: 'pa24-note' }, j.progress),
257
+ j.result && h('details', null, h('summary', null, '查看处理结果'), h('div', { className: 'pa24-note' }, j.result)),
258
+ j.child_session_id && button('external', '查看 Worker 会话', () => ctx.uiWorkspace.openSession({ childSessionId: j.child_session_id, parentSessionId: j.parent_session_id, mode: 'continuable' }), { className: 'pa24-quiet' }),
259
+ h('p', { className: 'pa24-meta' }, '事项编号:' + j.id)))
260
+ : [h(React.Fragment, { key: 'empty' },
261
+ empty('chat', 'blue', '下一件事,交给24私助', '在飞书或这里发一段话,机器人会安排合适的 Worker,并把进度与结果带回来。',
262
+ [button('chat', '与24私助对话', openRobot, { key: 'chat', className: 'pa24-primary', disabled: busy || !workspace })]),
263
+ h('div', { className: 'pa24-example' }, icon('info', { width: 16, height: 16 }), '可以说:“记一下:下周讨论新的合作方向。”'))];
264
+ content = h('div', null,
265
+ h('div', { className: 'pa24-stats' },
266
+ stats.map(([name, tone, label, value, hint]) =>
267
+ h('div', { key: label, className: 'pa24-stat' }, tile(name, tone), h('div', null, h('div', { className: 'pa24-stat-label' }, label), h('strong', null, value), h('small', null, hint))))),
268
+ h('section', { className: 'pa24-card' }, sectionHead('tasks', '正在办理的事项', '委托、进度与结果,在这里一目了然。', badge('PostgreSQL 业务账本', 'neutral', 'shield')), jobCards),
269
+ h('section', { className: 'pa24-card' }, sectionHead('pulse', '运行就绪', '配置、账本与接入的真实状态;不把进程在线当作全部就绪。'),
270
+ h('div', { className: 'pa24-ready' }, readyItems)));
271
+ } else if (tab === 'feishu') {
272
+ const check = state.diagnostics;
273
+ const cli = check ? check.cli : null;
274
+ const auth = check ? check.auth : null;
275
+ const diagnosis = (item, fallback = '尚未检查') => !item
276
+ ? badge(fallback, 'neutral', 'clock')
277
+ : item.state === 'ok'
278
+ ? badge('检查通过', 'blue', 'check')
279
+ : ['error', 'mismatch'].includes(item.state)
280
+ ? badge('需要处理', 'danger', 'alert')
281
+ : badge(item.state === 'missing' ? '待配置' : '待核验', 'amber', 'info');
282
+ const pendingOutbox = String((state.outbox || []).filter(o => ['pending', 'sending', 'unknown'].includes(o.status)).length) + ' 条(持久 Outbox)';
283
+ const resourceRows = [['folder', '文档目录', config ? config.folderToken : null, 'folder'], ['tasklist', '任务清单', config ? config.tasklistId : null, 'tasks'], ['calendar', '本人日历', config ? config.calendarId : null, 'calendar']].map(([id, label, value]) => {
284
+ const resource = check && check.resources ? check.resources.find(r => r.id === id) : null;
285
+ return h('div', { key: id, className: 'pa24-resource' },
286
+ h('div', { className: 'pa24-row' },
287
+ h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', null, label), h('p', null, value || '尚未配置')),
288
+ diagnosis(resource, value ? '尚未检查' : '待配置')),
289
+ resource ? h('p', null, resource.message) : null);
290
+ });
291
+ content = h('div', { className: 'pa24-grid' },
292
+ h('section', { className: 'pa24-card' },
293
+ sectionHead('plug', '飞书接入', '用自然语言配置,在这里查阅接入情况。',
294
+ h('div', { className: 'pa24-row' },
295
+ button('refresh', busy ? '检查中…' : '检查接入状态', () => rpc('action', { type: 'connection.check' }), { className: 'pa24-primary' }),
296
+ button('chat', '通过对话配置', openRobot)), 'blue'),
297
+ fields([
298
+ ['固定 CLI profile', badge(config ? config.larkProfile : '未配置', 'blue')],
299
+ ['接入模式', config && config.mode === 'feishu' ? '飞书模式' : '体验模式 · 未连接飞书'],
300
+ ['上次检查', check ? date(check.checkedAt, tz) : '尚未检查,点击右上方按钮开始'],
301
+ ]),
302
+ h('div', { className: 'pa24-helper' }, '可以说:“检查飞书接入,告诉我还缺什么。” 检查只读取,不创建飞书对象。')),
303
+ h('section', { className: 'pa24-card' },
304
+ h('div', { className: 'pa24-row pa24-between' }, h('h2', null, '机器人连接'), badge(state.transport && state.transport.connected ? '长连接已启动' : '未启动', state.transport && state.transport.connected ? 'blue' : 'neutral', 'plug')),
305
+ fields([
306
+ ['连接状态', (state.transport && state.transport.message) || '未启动'],
307
+ ['最近收到本人消息', date(state.transport ? state.transport.lastReceivedAt : null, tz)],
308
+ ['最近发送获平台确认', date(state.transport ? state.transport.lastSentAt : null, tz)],
309
+ ['待发消息', pendingOutbox],
310
+ ]),
311
+ h('p', { className: 'pa24-helper' }, '在飞书发送 /24pa 并收到回复后,再核对收发记录。长连接启动不代表端到端可用。')),
312
+ h('section', { className: 'pa24-card' },
313
+ h('div', { className: 'pa24-row pa24-between' }, h('h2', null, 'CLI 与用户授权'), diagnosis(auth)),
314
+ fields([
315
+ ['安装状态', cli ? cli.message : '尚未检查'],
316
+ ['CLI 版本', cli ? cli.version : '尚未检查'],
317
+ ['用户授权', auth ? auth.message : '尚未检查'],
318
+ ['CLI 用户', auth ? auth.userName : '尚未取得'],
319
+ ['用户令牌', auth ? auth.tokenStatus : '尚未确认'],
320
+ ]),
321
+ h('details', null, h('summary', null, '路径与身份明细'),
322
+ fields([['CLI 路径', cli ? cli.path : '尚未检查'], ['CLI open_id', auth ? auth.openId : '尚未取得'], ['配置主人', config ? config.ownerOpenId : '未配置']]))),
323
+ h('section', { className: 'pa24-card' },
324
+ sectionHead('folder', '使用的飞书资源', '检查结果仅代表可读性;写入由实际任务回执验证。'),
325
+ resourceRows,
326
+ h('details', null, h('summary', null, '配置来源与生效时间'),
327
+ fields([['配置来源', (workspace ? workspace.path : '') + '/AGENTS.md'], ['生效时间', date(workspace ? workspace.loadedAt : null, tz)], ['文件与生效版本', check && check.source ? check.source.message : '尚未检查']]))));
328
+ } else if (tab === 'memory') {
329
+ content = workspace && h(MemoryView, { key: workspace.path, workspace, openRobot: () => void run(openRobot) });
330
+ } else if (tab === 'workspace') {
331
+ const enabledNames = (config && config.enabledWorkers ? config.enabledWorkers : []).map(w => ({ memo: '备忘整理' }[w] || w)).join('、') || '无';
332
+ content = h('div', { className: 'pa24-grid' },
333
+ h(HealthBlock),
334
+ h('section', { className: 'pa24-card' },
335
+ sectionHead('folder', '当前工作区', '在 dsh 添加服务器目录,再选择绑定。空目录首次绑定时生成 AGENTS.md。'),
336
+ h('label', { style: { display: 'block', margin: '18px 0 12px' } },
337
+ h('span', { className: 'pa24-meta' }, '选择 dsh 工作区'),
338
+ h('select', { value: path, onChange: e => setPath(e.target.value), style: { marginTop: 6 } },
339
+ h('option', { value: '' }, '选择一个工作区…'),
340
+ (state.availableWorkspaces || []).map(w => h('option', { value: w.path, key: w.id }, w.title + ' · ' + w.path)))),
341
+ button('folder', '绑定所选工作区', () => rpc('action', { type: 'workspace.bind', path }), { disabled: busy || !path }),
342
+ h('div', { className: 'pa24-helper' }, '当前目录', h('p', { className: 'pa24-meta' }, workspace ? workspace.path : '未绑定'))),
343
+ h('section', { className: 'pa24-card' },
344
+ sectionHead('code', '当前生效配置', '配置源是工作区 AGENTS.md;凭据值由服务器环境或 CLI 授权保存。',
345
+ h('div', { className: 'pa24-row' },
346
+ button('chat', '通过对话修改配置', openRobot),
347
+ button('refresh', '重载 AGENTS.md', () => rpc('action', { type: 'workspace.reload' }), { className: 'pa24-quiet' })), 'blue'),
348
+ workspace && fields([
349
+ ['固定飞书 profile', config ? config.larkProfile : null],
350
+ ['时区', config ? config.timeZone : null],
351
+ ['同时处理数', config ? config.maxWorkers : null],
352
+ ['已启用 Worker', enabledNames],
353
+ ['飞书接入会话', workspace.accessSessionId || '未建立'],
354
+ ['本地助理会话', workspace.localSessionId || '未建立'],
355
+ ]),
356
+ workspace && workspace.configError && h('div', { role: 'alert', className: 'pa24-error' }, h('strong', null, '当前生效配置保留上一有效版本'), h('p', null, workspace.configError)),
357
+ h('details', null, h('summary', null, '查看完整配置 JSON'), h('pre', null, JSON.stringify(config, null, 2)))));
358
+ }
359
+ return h('main', { className: 'pa24', ref: main }, h('style', null, style), h('div', { className: 'pa24-wrap' },
360
+ h('header', { className: 'pa24-header' },
361
+ h('div', { className: 'pa24-identity' }, h('span', { className: 'pa24-brand' }, icon('bot')), h('div', null, h('div', { className: 'pa24-eyebrow' }, 'YOUR PERSONAL ASSISTANT'), h('h1', null, t('title')), h('p', { className: 'pa24-tagline' }, '日常交给我,重要的事由你决定。'))),
362
+ h('div', { className: 'pa24-row' }, button('chat', '与24私助对话', openRobot, { className: 'pa24-primary', disabled: busy || !workspace }), button('refresh', '刷新', () => refresh(), { className: 'pa24-quiet' }))),
363
+ h('div', { className: 'pa24-workspace-strip' }, icon('folder', { width: 17, height: 17 }), h('span', { className: 'pa24-path', title: workspace?.path }, workspace?.path || '尚未绑定工作区'), badge('工作区', 'neutral'), h('button', { type: 'button', className: 'pa24-quiet', onClick: () => selectTab('workspace') }, '查看配置', icon('chevron', { width: 14, height: 14 }))),
364
+ config?.mode === 'demo' && h('div', { className: 'pa24-notice pa24-tone-amber' }, icon('info', { width: 18, height: 18 }), h('div', null, h('strong', null, '体验模式'), ' · 未连接飞书;业务账本使用 PostgreSQL,备忘仅入账本。')),
365
+ (error || connError) && h('div', { role: 'alert', className: 'pa24-error' }, h('strong', null, connError ? '连接暂时中断,以下为上次读取的状态' : '操作未完成'), h('p', null, error || connError)),
366
+ h('nav', { className: 'pa24-tabs', role: 'tablist', 'aria-label': '24私助工作区导航' }, tabItems.map(([id, name, tone], index) => h('button', { key: id, id: 'pa24-tab-' + id, type: 'button', role: 'tab', className: 'pa24-tone-' + tone, 'aria-selected': tab === id, 'aria-controls': 'pa24-content', tabIndex: tab === id ? 0 : -1, onClick: () => selectTab(id), onKeyDown: e => {
367
+ const next = e.key === 'ArrowRight' ? (index + 1) % tabItems.length : e.key === 'ArrowLeft' ? (index + tabItems.length - 1) % tabItems.length : e.key === 'Home' ? 0 : e.key === 'End' ? tabItems.length - 1 : null;
368
+ if (next === null) return; e.preventDefault(); selectTab(tabItems[next][0]); e.currentTarget.parentElement.querySelectorAll('[role=tab]')[next].focus();
369
+ } }, icon(name, { width: 17, height: 17 }), t(id)))),
370
+ h('div', { id: 'pa24-content', role: 'tabpanel', 'aria-labelledby': 'pa24-tab-' + tab, tabIndex: 0 }, content),
371
+ h('footer', { className: 'pa24-footer' }, icon('shield', { width: 13, height: 13 }), '业务账本使用 PostgreSQL;配置与记忆通过对话维护,界面不伪造成功。')));
372
+ }
373
+ const SidebarIcon = () => icon('bot', { width: 22, height: 22 });
374
+ ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'pa24' }, Panel));
375
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', id: 'pa24', order: 24, label: () => t('panel') }, SidebarIcon));
376
+ },
377
+ };
378
+ },
379
+ });