lantern-harness 0.1.1 → 0.1.3
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/CHANGELOG.md +74 -0
- package/README.md +10 -10
- package/builtin/session/lh-spaces.mjs +105 -0
- package/cli/acp-server.mjs +15 -1
- package/cli/data-root.mjs +80 -3
- package/cli/lantern.mjs +27 -10
- package/cli/server.mjs +4 -0
- package/cli/spaces.mjs +278 -38
- package/config/app.defaults.json +4 -2
- package/dist/500.html +9 -5
- package/dist/_astro/Base.ByB0D4ZA.css +1 -0
- package/dist/_astro/{Base.astro_astro_type_script_index_0_lang.lxEwQ1R4.js → Base.astro_astro_type_script_index_0_lang.Cg_8YLVA.js} +1 -1
- package/dist/_astro/CaseGenerateApp.BlZyjWEr.js +9 -0
- package/dist/_astro/CasesLibrary.C76La3Xj.js +1 -0
- package/dist/_astro/ChatApp.gfsPLvan.js +269 -0
- package/dist/_astro/ColumnResize.Bs6GL3DJ.js +1 -0
- package/dist/_astro/DatePicker.BdwFRioL.js +1 -0
- package/dist/_astro/{DocsFileBrowser.DkE26UTR.js → DocsFileBrowser.BxAyrkkw.js} +1 -1
- package/dist/_astro/KnowledgeLibrary.Cv488Hot.js +1 -0
- package/dist/_astro/NavLibraries.ALZ9e12z.js +1 -0
- package/dist/_astro/PluginsPage.B2uxCiHL.js +1 -0
- package/dist/_astro/{PluginsPage.BNzpPg2H.js → PluginsPage.DEkyOfQE.js} +2 -2
- package/dist/_astro/PromptsLibrary.BUC1OiCH.js +1 -0
- package/dist/_astro/RequirementsLibrary.D3olQI63.js +1 -0
- package/dist/_astro/{SearchableSelect.DMS2XmXc.js → SearchableSelect.DP74JRWW.js} +1 -1
- package/dist/_astro/SelectDropdown.D2i7-lQC.js +1 -0
- package/dist/_astro/{SettingsDialog.DD-XOFEX.js → SettingsDialog.CrMDjSLJ.js} +1 -1
- package/dist/_astro/SlideOverDrawer.C7UVeCX1.js +1 -0
- package/dist/_astro/SpaceSwitcher.Wf8EPYUx.js +7 -0
- package/dist/_astro/TasksBoard.2j7rGT9O.js +1 -0
- package/dist/_astro/TemplatesLibrary.J0x7yIcY.js +1 -0
- package/dist/_astro/TodosBoard.BugeB7_7.js +1 -0
- package/dist/_astro/app-config.vqDkEMS3.js +1 -0
- package/dist/_astro/doc-roots.DAJP9Svr.js +1 -0
- package/dist/_astro/i18n.9qZsrs3u.js +7 -0
- package/dist/_astro/{open-docs-in-session.D3JCwMWB.js → open-docs-in-session.fLwnA5qh.js} +1 -1
- package/dist/_astro/plugins.DSUPSizB.js +1 -0
- package/dist/_astro/project-docs.Cd2gKPKh.js +1 -0
- package/dist/_astro/{settings.BoQ0e0m1.js → settings.CkbipdPI.js} +2 -2
- package/dist/_astro/{useListAnim.CnbC_9MC.js → useListAnim.Y3N2budK.js} +1 -1
- package/dist/cases/generate/index.html +10 -6
- package/dist/cases/library/index.html +10 -6
- package/dist/docs/changelog/index.html +80 -6
- package/dist/docs/install/index.html +26 -12
- package/dist/docs/manual/index.html +74 -11
- package/dist/index.html +10 -6
- package/dist/knowledge/index.html +10 -6
- package/dist/knowledge//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/347/274/226/345/206/231/350/247/204/350/214/203/index.html +9 -5
- package/dist/plugins/index.html +10 -6
- package/dist/prompts/index.html +10 -6
- package/dist/requirements/index.html +10 -6
- package/dist/tasks/index.html +253 -0
- package/dist/templates/index.html +10 -6
- package/dist/templates//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +9 -5
- package/dist/todos/index.html +253 -0
- package/docs//345/205/250/345/261/200/346/217/222/344/273/266.md +2 -2
- package/docs//345/256/211/350/243/205/346/211/213/345/206/214.md +14 -12
- package/docs//346/223/215/344/275/234/346/211/213/345/206/214.md +73 -23
- package/lh-plugins/lh-project-docs.mjs +2 -0
- package/lh-plugins/lh-tasks.mjs +223 -0
- package/lh-plugins/lh-todos.mjs +297 -0
- package/lh-plugins/storage-domain.mjs +1 -0
- package/package.json +1 -1
- package/dist/_astro/Base.B9f_iFAZ.css +0 -1
- package/dist/_astro/CaseGenerateApp.Br4uKjun.js +0 -9
- package/dist/_astro/CasesLibrary.CpPoVQ4P.js +0 -1
- package/dist/_astro/ChatApp.CYSPlSNP.js +0 -258
- package/dist/_astro/KnowledgeLibrary.DZinlAWn.js +0 -1
- package/dist/_astro/NavLibraries.EayBsg5g.js +0 -1
- package/dist/_astro/PluginsPage.CY61glLU.js +0 -1
- package/dist/_astro/PromptsLibrary.DfrrCjs7.js +0 -1
- package/dist/_astro/RequirementsLibrary.Dt0Gj6Nr.js +0 -1
- package/dist/_astro/SelectDropdown.Dc7QaPbH.js +0 -1
- package/dist/_astro/SpaceSwitcher.Yy2w8MDF.js +0 -7
- package/dist/_astro/TemplatesLibrary.uK-SjK1z.js +0 -1
- package/dist/_astro/app-config.CpjKWnzM.js +0 -1
- package/dist/_astro/i18n.BVvhz-vw.js +0 -7
- package/dist/_astro/plugins.CrpY8UI9.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 安装手册
|
|
2
2
|
|
|
3
|
-
要求本地已安装 [Node.js](https://nodejs.org/)(需 22.22
|
|
3
|
+
要求本地已安装 [Node.js](https://nodejs.org/)(需 22.22 及以上)。装好后在侧栏「设置」里配置模型 API Key(自带密钥,仅存本机);默认地址 `http://127.0.0.1:4321/`。
|
|
4
4
|
|
|
5
5
|
## 方式一:本地服务(推荐)
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ npx lantern-harness
|
|
|
12
12
|
|
|
13
13
|
执行后会安装全局短命令 **`lantern`**。产品名 **Lantern Harness**;npm 包名 `lantern-harness`,命令 `lantern`。
|
|
14
14
|
|
|
15
|
-
- 升级:`lantern
|
|
15
|
+
- 升级:`lantern update`(最新版本)或 `lantern update <版本>`(指定版本)
|
|
16
16
|
- 卸载命令:`lantern uninstall`(保留数据空间)
|
|
17
17
|
- 查看可用版本:`lantern list`
|
|
18
18
|
|
|
@@ -53,9 +53,9 @@ lantern logs # 查看服务日志
|
|
|
53
53
|
lantern doctor # 检查运行环境与配置是否正常
|
|
54
54
|
lantern help # 显示操作命令列表
|
|
55
55
|
lantern version # 显示当前版本号
|
|
56
|
-
lantern list #
|
|
57
|
-
lantern
|
|
58
|
-
lantern
|
|
56
|
+
lantern list # 显示可用版本列表
|
|
57
|
+
lantern update # 更新到最新版本
|
|
58
|
+
lantern update 0.1.x # 更新到指定版本
|
|
59
59
|
lantern uninstall # 卸载本机 lantern 命令(保留数据空间)
|
|
60
60
|
lantern wipe # 删除当前数据空间(需确认;不可恢复)
|
|
61
61
|
lantern ask # 进阶:无界面单轮问答,lantern ask <提示词>
|
|
@@ -69,14 +69,16 @@ lantern acp # 进阶:以 ACP 子集供外部编排驱动无 UI Agen
|
|
|
69
69
|
|
|
70
70
|
### 5. 首次进入看到什么
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
新建空间默认开启「提示词管理」,其余插件默认关闭,所以侧栏只有「我的工作台」「提示词管理」「帮助中心」三项。每个空间都会自动带上自己的「默认提示词.md」,可随时在「提示词管理」或「空间管理 → 编辑」中修改。需要用例 / 知识库 / 任务 / 待办 / 记忆 / 插件开发等入口时,进入「插件」页在「扩展插件」里按分类开启:
|
|
73
73
|
|
|
74
|
-
| 分类 | 插件
|
|
75
|
-
| ---------- |
|
|
76
|
-
| 内容库 |
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
74
|
+
| 分类 | 插件 | 新建空间默认 |
|
|
75
|
+
| ---------- | -------------------------------------------- | ------------ |
|
|
76
|
+
| 内容库 | 用例管理、知识库管理、需求文档库、测试模板库 | 关 |
|
|
77
|
+
| 内容库 | 提示词管理 | 开(内置) |
|
|
78
|
+
| 生成能力 | 生成测试用例、生成提示词、生成知识库 | 关 |
|
|
79
|
+
| 记忆与检索 | 知识库检索、会话记忆 | 关 |
|
|
80
|
+
| 待办与效率 | 任务管理、待办事项 | 关 |
|
|
81
|
+
| 开发工具 | 插件开发 | 关 |
|
|
80
82
|
|
|
81
83
|
## 方式二:源码启动
|
|
82
84
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
界面从左到右为:左侧导航(功能入口)→ 中间内容区(工作台 / 内容库)→ 底部「设置」。首次使用请先完成模型配置。
|
|
4
4
|
|
|
5
|
-
| 区域 | 内容
|
|
6
|
-
| -------- |
|
|
7
|
-
| 左侧导航 |
|
|
8
|
-
| 底部设置 | 通用设置与模型(BYOK)配置
|
|
9
|
-
| 帮助中心 | 版本日志 / 操作手册 / 安装手册,以及「问题与建议」外链
|
|
5
|
+
| 区域 | 内容 |
|
|
6
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| 左侧导航 | 新建空间默认有「我的工作台」「提示词管理」「帮助中心」;提示词管理固定在帮助中心上方、不参与分组排序,其余内容库开启插件后才出现用例管理 / 知识库管理 / 需求文档库 / 测试模板库(可自行建分组) |
|
|
8
|
+
| 底部设置 | 通用设置与模型(BYOK)配置 |
|
|
9
|
+
| 帮助中心 | 版本日志 / 操作手册 / 安装手册,以及「问题与建议」外链 |
|
|
10
10
|
|
|
11
11
|
## 模型配置(BYOK,自带密钥)
|
|
12
12
|
|
|
@@ -91,7 +91,9 @@
|
|
|
91
91
|
| `知识库管理/` | 规范 / 方法论 / 环境约定 |
|
|
92
92
|
| `测试模板库/` | 用例与报告模板 |
|
|
93
93
|
|
|
94
|
-
> 目录**按插件开关懒建**:新建 / 首次选中的数据空间只有隐藏的 `.lantern/` 元数据,启用对应内容库插件后才出现上表目录并补入种子文件;旧版本留下的空目录(只有 `.gitkeep
|
|
94
|
+
> 目录**按插件开关懒建**:新建 / 首次选中的数据空间只有隐藏的 `.lantern/` 元数据,启用对应内容库插件后才出现上表目录并补入种子文件;旧版本留下的空目录(只有 `.gitkeep`)会自动收起,已有文件的目录不动。**`提示词管理/` 是唯一例外**:每个空间都有默认提示词,因此该目录与 `默认提示词.md` 会始终铺好。
|
|
95
|
+
|
|
96
|
+
> 「空间管理」默认展示全部空间:右上角「+ 新增空间」新建并立即写入默认提示词;每行的「编辑」可修改空间名称、提示词要求与默认提示词正文,保存后工作台立即生效。
|
|
95
97
|
|
|
96
98
|
> 旧版英文目录(`requirements/` / `cases/` / `prompts/` / `knowledge/` / `templates/`)在启动时**自动改名**为上述中文目录,无需手工迁移;若目标目录已存在则不覆盖。
|
|
97
99
|
|
|
@@ -99,6 +101,14 @@
|
|
|
99
101
|
|
|
100
102
|
把一份空间文件夹(带 `.lantern/space.json` 清单)整体拷进数据目录后,在「空间管理」点「扫描文件夹」,即可纳入列表:名称以目录名为准,清单里的 id 与已有空间撞车时自动换新。
|
|
101
103
|
|
|
104
|
+
「空间管理」里的「复制」是另一种做法:在数据目录内**逐文件复制**出一个新空间(含 `.lantern/local` 下的待办、任务、会话、记忆与配置),新空间直接出现在列表里、不需要再扫描。
|
|
105
|
+
|
|
106
|
+
- 复制会跳过 `node_modules` / `.git` / `.astro` / `dist` 与 `.DS_Store`;也会跳过可重建的派生缓存(`.lantern/local/session-query.sqlite` 及其 `-wal` / `-shm`、`.lantern/local/spill/`),避免拷到写了一半的文件。
|
|
107
|
+
- 复制品里的工作区注册表会被改指新目录,所以新空间的「工作区」停在自己身上,而不是被复制的那个空间。
|
|
108
|
+
- 有会话正在运行时不允许复制(与切换空间一致,先停止再操作)。
|
|
109
|
+
- 复制**不会**自动切过去;切到新空间时页面会重载,待办 / 任务等按新空间的数据重新加载。
|
|
110
|
+
- 浏览器本地存储里按空间 id 记的界面偏好(列宽、侧栏展开状态等)不会跟着复制,新空间用默认值。
|
|
111
|
+
|
|
102
112
|
同一目录里其它**不是空间**的文件夹会在扫描后列出来,勾选并确认才纳入:
|
|
103
113
|
|
|
104
114
|
- 纳入只把文件夹登记进列表,**不写入 `.lantern/space.json`,也不铺内容目录、不播种子**,文件夹内容保持原样。
|
|
@@ -177,23 +187,63 @@ version: '1.0' # 可选
|
|
|
177
187
|
工作台会话栏「新会话」下方点「插件」进入:
|
|
178
188
|
|
|
179
189
|
- **我的插件**(`/plugins/`):分「扩展插件 / 会话插件 / 全局插件」三个 Tab(默认打开扩展插件),Tab 标题带数量;点击卡片展开详情。扩展为可配置项,可开关;开关状态存本机。插件随仓库代码发布,不提供远程安装 / 更新。
|
|
180
|
-
- 扩展插件按「内容库 / 生成能力 / 记忆与检索 /
|
|
181
|
-
|
|
182
|
-
| 分类 | 插件 | 新建空间默认 | 说明
|
|
183
|
-
| ---------- | ------------ | ------------ |
|
|
184
|
-
| 内容库 | 用例管理 | 关 | 侧栏进入用例库;关闭后入口隐藏,「生成测试用例」一并关闭;磁盘文件仍保留
|
|
185
|
-
| 内容库 | 提示词管理 |
|
|
186
|
-
| 内容库 | 知识库管理 | 关 | 侧栏进入知识库;关闭后入口隐藏,「生成知识库」一并关闭;磁盘文件仍保留
|
|
187
|
-
| 内容库 | 需求文档库 | 关 | 侧栏进入需求文档库;关闭后入口隐藏,生成用例时不再引用需求文档;磁盘文件仍保留
|
|
188
|
-
| 内容库 | 测试模板库 | 关 | 侧栏进入测试模板库;关闭后入口隐藏,生成用例时不再选用模板;磁盘文件仍保留
|
|
189
|
-
| 生成能力 | 生成测试用例 | 关 | 配置类型 / 输出格式 / 需求 / 模板 / 提示词后对话生成用例,写入 `用例管理/`(需本地服务落盘)
|
|
190
|
-
| 生成能力 | 生成提示词 | 关 | 配置类型后对话生成提示词,质检通过写入 `提示词管理/`(需本地服务)
|
|
191
|
-
| 生成能力 | 生成知识库 | 关 | 配置分类后对话生成规范 / 方法论等,质检通过写入 `知识库管理/`(需本地服务)
|
|
192
|
-
| 记忆与检索 | 知识库检索 | 关 | 工作台输入 `@` 引用 `知识库管理/`、`测试模板库/` 与工作区文档
|
|
193
|
-
| 记忆与检索 | 会话记忆 | 关 | 长期记忆 / 项目关键 / 项目日志等记忆轨 + 待确认队列;`.lantern/local/memories/`(需本地服务)
|
|
194
|
-
|
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
- 扩展插件按「内容库 / 生成能力 / 记忆与检索 / 待办与效率 / 开发工具」五类分组展示,分类标题带数量,可点击折叠 / 展开;分类与插件注册表的 `category` 字段一致。
|
|
191
|
+
|
|
192
|
+
| 分类 | 插件 | 新建空间默认 | 说明 |
|
|
193
|
+
| ---------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------- |
|
|
194
|
+
| 内容库 | 用例管理 | 关 | 侧栏进入用例库;关闭后入口隐藏,「生成测试用例」一并关闭;磁盘文件仍保留 |
|
|
195
|
+
| 内容库 | 提示词管理 | 开(内置) | 侧栏进入提示词库,并维护当前空间的「默认提示词」;每个空间都需要,因此常开不可关闭 |
|
|
196
|
+
| 内容库 | 知识库管理 | 关 | 侧栏进入知识库;关闭后入口隐藏,「生成知识库」一并关闭;磁盘文件仍保留 |
|
|
197
|
+
| 内容库 | 需求文档库 | 关 | 侧栏进入需求文档库;关闭后入口隐藏,生成用例时不再引用需求文档;磁盘文件仍保留 |
|
|
198
|
+
| 内容库 | 测试模板库 | 关 | 侧栏进入测试模板库;关闭后入口隐藏,生成用例时不再选用模板;磁盘文件仍保留 |
|
|
199
|
+
| 生成能力 | 生成测试用例 | 关 | 配置类型 / 输出格式 / 需求 / 模板 / 提示词后对话生成用例,写入 `用例管理/`(需本地服务落盘) |
|
|
200
|
+
| 生成能力 | 生成提示词 | 关 | 配置类型后对话生成提示词,质检通过写入 `提示词管理/`(需本地服务) |
|
|
201
|
+
| 生成能力 | 生成知识库 | 关 | 配置分类后对话生成规范 / 方法论等,质检通过写入 `知识库管理/`(需本地服务) |
|
|
202
|
+
| 记忆与检索 | 知识库检索 | 关 | 工作台输入 `@` 引用 `知识库管理/`、`测试模板库/` 与工作区文档 |
|
|
203
|
+
| 记忆与检索 | 会话记忆 | 关 | 长期记忆 / 项目关键 / 项目日志等记忆轨 + 待确认队列;`.lantern/local/memories/`(需本地服务) |
|
|
204
|
+
| 待办与效率 | 任务管理 | 关 | 侧栏进入任务列表:任务池 / 进行中 / 已完成 / 已关闭,自动汇总任务下待办的数量与完成进度(需本地服务) |
|
|
205
|
+
| 待办与效率 | 待办事项 | 关 | 侧栏进入待办列表:筛选、行内改状态、手动增删改;工作台会话可用工具记录同一份待办(需本地服务) |
|
|
206
|
+
| 开发工具 | 插件开发 | 关 | 固定提示词开发扩展插件:先评估并用选项确认,确认后才改代码,完成后并入「扩展插件」(需本地服务) |
|
|
207
|
+
|
|
208
|
+
> **新建空间默认开启「提示词管理」,其余插件默认关闭**:每个空间都会带上自己的「默认提示词.md」,因此提示词管理是内置常开项;其余入口与「生成」能力仍要在「我的插件」里按分类手动开启,已有空间也保持原开关。种子文件(默认提示词、模板)不会让其它插件自动打开,只有用户自己放进空间的内容才会触发首次识别。
|
|
209
|
+
|
|
210
|
+
### 待办事项
|
|
211
|
+
|
|
212
|
+
「我的插件」→「待办与效率」→「待办事项」开启后,侧栏出现「待办事项」入口(`/todos/`)。
|
|
213
|
+
|
|
214
|
+
- 数据落在当前数据空间的 `.lantern/local/todos.json`,随空间走、不进 git;切换空间后看到的是各空间自己的待办。
|
|
215
|
+
- 字段:待办名称、待办状态(待开始 / 进行中 / 已完成 / 已取消)、紧急程度(重要紧急 / 重要不紧急 / 紧急不重要 / 普通)、所属任务、开始日期、截止日期、结束日期、备注。
|
|
216
|
+
- **结束日期是只读派生值**:状态改为「已完成 / 已取消」时自动记为当天(再改其他字段不会刷新);状态改回「待开始 / 进行中」时清空,界面上显示「待办未完成」。表格行内、抽屉、AI 工具(`todo_update`)改状态都按同一规则处理。
|
|
217
|
+
- 新建待办(抽屉与「+ 新增记录」)的**开始日期 / 截止日期默认是当天**,可直接改。
|
|
218
|
+
- 「所属任务」从「任务管理」里选(需同时开启该插件),选中后写回任务 id 与任务名称;**已关闭的任务不出现在选项里**。待办关联任务时,任务会从「任务池 / 已完成」被拉回「进行中」(已关闭的不动);打开待办页时也会对「仍有未完成待办」的已关联任务做同样处理。任务管理开启时,旧数据里只存了任务名的待办会按名称自动补上关联,任务管理关闭时该列显示「任务管理插件未启用」,原任务名不会丢。
|
|
219
|
+
- 「待办周期」与「剩余天数」由日期自动算出(含首尾);未完成且过了截止日期显示「已延期」,存储状态不变。
|
|
220
|
+
- 表格按飞书多维表格的方式展示:默认按「待办状态」分组(进行中 / 待开始 / 已延期 / 已完成 / 已取消),每组标题带记录数,点标题可折叠;顶部的分组下拉可切换为「按紧急程度分组」或「不分组」。
|
|
221
|
+
- 单元格直接改:点名称 / 所属任务 / 日期 / 备注即可编辑,回车或点空白处保存,Esc 放弃;状态下拉与紧急程度下拉在行内改完立即生效(改状态后会自动换到对应分组)。
|
|
222
|
+
- 日期格不再显示系统小日历图标:点一下弹出白色卡片日历(可翻月,底部「清除 / 今天」),选完立即保存。
|
|
223
|
+
- 工具栏与单元格里的下拉都是卡片菜单(白底圆角),状态 / 紧急程度用颜色点和选中底色区分,不显示对钩,不会弹出系统原生下拉。
|
|
224
|
+
- 每组底部有「+ 新增记录」(手动新增入口):点开后在组内展开一行,包含全部列(名称 / 状态 / 紧急程度 / 所属任务 / 开始日期 / 截止日期 / 备注;结束日期随状态自动生成,不在这里填);没有单独的保存 / 取消按钮——填好名称后回车,或直接点页面空白处即自动保存,完全没填内容时点空白处则自动收起,Esc 放弃。顶部「新建待办」会从右侧滑出记录抽屉,按顺序填写名称、状态、任务详情、日期与备注后保存;没有数据时只保留右上角的「新建待办」按钮。
|
|
225
|
+
- 抽屉里的「待办状态」整行都可点:点击范围一直延伸到「待办名称」输入框的右边界,胶囊只是选中值的样子,点行的任意位置都会展开状态选项。
|
|
226
|
+
- 行尾的展开按钮可用同一个抽屉回看 / 修改整条记录;抽屉里的「待办周期」「剩余天数」是只读派生值,会随日期即时计算。
|
|
227
|
+
- 顶部可搜索,并按紧急程度筛选;支持批量删除,行尾「操作」列可删除单条,都会二次弹窗确认;表格右侧列较多时可横向滚动。
|
|
228
|
+
- 列宽可拖:把鼠标放到表头字段的右边缘,出现竖线后左右拖即可调整;默认宽度就是设计稿宽度。调过的列宽存在浏览器本地(localStorage),刷新、切换页面后保持,不会写进数据空间。
|
|
229
|
+
- 新增行没有任何内容时,点页面其它位置会自动收起;已经填了名称则点空白处(或回车)即自动保存,不再有单独的 ✓ / ✕ 按钮。
|
|
230
|
+
- 「我的工作台」对话上方的 Tab 里有「待办」(待办插件开启时,排在「回合」右侧):默认「全部待办」,按 已逾期 / 今天截止 / 接下来 / 没有截止日期 / 已完成 分桶(今日与其它待办都在里面);也可只看「未完成」或「已完成」。每行显示待办名称、紧急程度,以及第二行的截止日期、剩余天数(逾期会标红)和所属任务;点左侧圆圈即可标记完成 / 恢复,右上角「打开待办页」跳到完整表格。停在 Tab 上每 15 秒自动刷新,AI 在会话里写入后不用手动刷。
|
|
231
|
+
- 输入框左侧「+」→「计划」下面会按插件开关给出「待办」「任务」两个手动入口(对应插件关闭时入口直接隐藏,不会触发新增):点开后弹出同一张确认卡片,手动入口的最后一个按钮是「提交」。待办分两页:第一页填待办自身字段(名称 / 状态 / 紧急程度 / 开始日期 / 截止日期 / 备注,开始 / 截止默认今天),第二页选「所属任务」并只读展示该任务的名称、状态、紧急程度与起止日期;选中任务后,任务有值的开始日期 / 截止日期 / 紧急程度会自动带到第一页(紧急程度仅在待办未设置时带出),仍可再改。任务没有所属任务,一页填完(名称 / 状态 / 紧急程度 / 开始日期 / 截止日期 / 备注)。填好「提交」直接落库,不经过模型;点「取消」或点卡片外面任意位置(包括切到别的 Tab)只会播放收起动画并放弃这次新增,不会写入待办 / 任务。
|
|
232
|
+
- 工作台里直接说「记一条待办:周五前提交测试报告」,AI 会调用 `todo_add`;写入前聊天区会弹出确认卡片,同样分两页(第一页待办字段,第二页选所属任务并带出任务的日期与紧急程度),点「确认新增」才落库;点「取消」或点卡片外面会收起卡片、不写任何数据。查进度用 `todo_list`,完成用 `todo_update`。停在该页时会定时刷新,AI 在会话里写入后不用手动刷新。
|
|
233
|
+
|
|
234
|
+
### 任务管理
|
|
235
|
+
|
|
236
|
+
「我的插件」→「待办与效率」→「任务管理」开启后,侧栏出现「任务管理」入口(`/tasks/`)。
|
|
237
|
+
|
|
238
|
+
- 数据落在当前数据空间的 `.lantern/local/tasks.json`;切换空间后看到各空间自己的任务。
|
|
239
|
+
- 顶部六张统计卡片:全部任务、任务池、进行中、已完成、已关闭、需要紧急(重要紧急 + 紧急不重要)。
|
|
240
|
+
- 列表字段:任务描述、任务状态、紧急程度、开始日期、截止日期、任务周期、待办数量、完成进度、备注。
|
|
241
|
+
- 新建任务的开始日期 / 截止日期默认是当天,可直接改。
|
|
242
|
+
- 「任务周期」由开始 / 截止日期算出;「待办数量」与「完成进度」按待办里的「所属任务」实时汇总(待办完成状态变化后自动刷新)。待办事项插件关闭时,这两列显示「待办事项未启用」。
|
|
243
|
+
- 状态 / 紧急程度在行内用卡片菜单改;日期点开卡片日历;点任务名称或用行尾展开按钮打开右侧抽屉,可改名称、日期与备注,并看到该任务下已关联的待办。
|
|
244
|
+
- 工作台里也可以让 AI 建任务:说「新建一个任务:…」会调用 `task_add`,同样先弹确认卡片(一页:名称 / 状态 / 紧急程度 / 开始 / 截止日期 / 备注),确认后才创建;点「取消」或点卡片外面不会写入。查询用 `task_list`、改字段用 `task_update`、删除用 `task_delete`。
|
|
245
|
+
- 删除任务不会删待办,只是解除关联(待办里的「所属任务」变为未关联)。
|
|
246
|
+
- 列宽同样可拖表头右边缘调整,与待办页一致;表头、行高、状态胶囊、行尾展开 / 删除按钮的样式也与待办页保持同一套。
|
|
197
247
|
|
|
198
248
|
### 插件开发
|
|
199
249
|
|
|
@@ -718,6 +718,8 @@ function readJsonOr(fp, fallback) {
|
|
|
718
718
|
* @param {string} dirName 中文内容目录名
|
|
719
719
|
*/
|
|
720
720
|
export function isContentRootEnabled(spaceDir, dirName) {
|
|
721
|
+
// 提示词管理是内置常开项:每个空间都要有「默认提示词」,不受插件开关影响
|
|
722
|
+
if (dirName === DOC_ROOT_DIRS.prompts.dirName) return true;
|
|
721
723
|
const key = Object.keys(DOC_ROOT_DIRS).find((k) => DOC_ROOT_DIRS[k].dirName === dirName);
|
|
722
724
|
if (!key) return false;
|
|
723
725
|
const pluginId = CONTENT_ROOT_PLUGINS[key];
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务管理存储(空间级,独立插件)。
|
|
3
|
+
*
|
|
4
|
+
* 数据落在 `<当前数据空间>/.lantern/local/tasks.json`:跟空间走、不跟 git 走。
|
|
5
|
+
* 待办通过 `taskId` 关联到这里;任务周期 / 待办数量 / 完成进度都是派生值,不落库。
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { getDataRoot } from '../builtin/session/space-context.mjs';
|
|
12
|
+
import { spaceLocalFile } from '../builtin/session/space-layout.mjs';
|
|
13
|
+
|
|
14
|
+
const PREFIX = '/__tb_tasks';
|
|
15
|
+
const FILE_NAME = 'tasks.json';
|
|
16
|
+
const VERSION = 1;
|
|
17
|
+
|
|
18
|
+
export const TASK_STATUSES = ['pool', 'in_progress', 'done', 'closed'];
|
|
19
|
+
export const TASK_PRIORITIES = ['urgent_important', 'important', 'urgent', 'normal'];
|
|
20
|
+
|
|
21
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
22
|
+
|
|
23
|
+
function sendJson(res, status, body) {
|
|
24
|
+
res.statusCode = status;
|
|
25
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
26
|
+
res.end(JSON.stringify(body));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readBody(req) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
req.on('data', (c) => chunks.push(c));
|
|
33
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
34
|
+
req.on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function storeFile(root) {
|
|
39
|
+
return spaceLocalFile(root, FILE_NAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readStore(root) {
|
|
43
|
+
const fp = storeFile(root);
|
|
44
|
+
try {
|
|
45
|
+
const raw = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
46
|
+
const items = Array.isArray(raw?.items) ? raw.items.map(normalizeStored).filter(Boolean) : [];
|
|
47
|
+
return { version: VERSION, items };
|
|
48
|
+
} catch {
|
|
49
|
+
return { version: VERSION, items: [] };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 先写临时文件再 rename:中途失败也不会留下半份 JSON */
|
|
54
|
+
function writeStore(root, store) {
|
|
55
|
+
const fp = storeFile(root);
|
|
56
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
57
|
+
const tmp = `${fp}.${process.pid}.tmp`;
|
|
58
|
+
fs.writeFileSync(tmp, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
|
|
59
|
+
fs.renameSync(tmp, fp);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function newId() {
|
|
63
|
+
return `tk_${crypto.randomBytes(8).toString('hex')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function text(value, max = 2000) {
|
|
67
|
+
return typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function dateOrEmpty(value) {
|
|
71
|
+
const s = text(value, 10);
|
|
72
|
+
return DATE_RE.test(s) ? s : '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function oneOf(value, allowed, fallback) {
|
|
76
|
+
const s = text(value, 40);
|
|
77
|
+
return allowed.includes(s) ? s : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 读回来的旧数据也走一遍归一化,避免手工改坏的文件把页面拖垮 */
|
|
81
|
+
function normalizeStored(raw) {
|
|
82
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
83
|
+
const title = text(raw.title, 200);
|
|
84
|
+
if (!title) return null;
|
|
85
|
+
const status = oneOf(raw.status, TASK_STATUSES, 'pool');
|
|
86
|
+
const now = new Date().toISOString();
|
|
87
|
+
return {
|
|
88
|
+
id: text(raw.id, 64) || newId(),
|
|
89
|
+
title,
|
|
90
|
+
status,
|
|
91
|
+
priority: oneOf(raw.priority, TASK_PRIORITIES, ''),
|
|
92
|
+
startDate: dateOrEmpty(raw.startDate),
|
|
93
|
+
dueDate: dateOrEmpty(raw.dueDate),
|
|
94
|
+
note: text(raw.note),
|
|
95
|
+
source: raw.source === 'ai' ? 'ai' : 'manual',
|
|
96
|
+
sessionId: text(raw.sessionId, 120),
|
|
97
|
+
createdAt: text(raw.createdAt, 40) || now,
|
|
98
|
+
updatedAt: text(raw.updatedAt, 40) || now,
|
|
99
|
+
doneAt: text(raw.doneAt, 40) || null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 入参归一化:只认白名单字段,缺省值由调用方(新建/更新)决定 */
|
|
104
|
+
function normalizePatch(raw, base) {
|
|
105
|
+
const patch = {};
|
|
106
|
+
if (!raw || typeof raw !== 'object') return patch;
|
|
107
|
+
if ('title' in raw) patch.title = text(raw.title, 200);
|
|
108
|
+
if ('status' in raw) patch.status = oneOf(raw.status, TASK_STATUSES, base.status);
|
|
109
|
+
if ('priority' in raw) patch.priority = oneOf(raw.priority, TASK_PRIORITIES, '');
|
|
110
|
+
if ('startDate' in raw) patch.startDate = dateOrEmpty(raw.startDate);
|
|
111
|
+
if ('dueDate' in raw) patch.dueDate = dateOrEmpty(raw.dueDate);
|
|
112
|
+
if ('note' in raw) patch.note = text(raw.note);
|
|
113
|
+
return patch;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {{ mode?: 'manual' | 'ai', sessionId?: string }} [meta]
|
|
118
|
+
*/
|
|
119
|
+
function buildItem(raw, meta = {}) {
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
const status = oneOf(raw?.status, TASK_STATUSES, 'pool');
|
|
122
|
+
return {
|
|
123
|
+
id: newId(),
|
|
124
|
+
title: text(raw?.title, 200),
|
|
125
|
+
status,
|
|
126
|
+
priority: oneOf(raw?.priority, TASK_PRIORITIES, ''),
|
|
127
|
+
startDate: dateOrEmpty(raw?.startDate),
|
|
128
|
+
dueDate: dateOrEmpty(raw?.dueDate),
|
|
129
|
+
note: text(raw?.note),
|
|
130
|
+
source: meta.mode === 'ai' ? 'ai' : 'manual',
|
|
131
|
+
sessionId: text(meta.sessionId, 120),
|
|
132
|
+
createdAt: now,
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
doneAt: status === 'done' ? now : null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @returns {import('vite').Plugin}
|
|
140
|
+
*/
|
|
141
|
+
export function lhTasksPlugin() {
|
|
142
|
+
return {
|
|
143
|
+
name: 'lh-tasks',
|
|
144
|
+
configureServer(server) {
|
|
145
|
+
server.middlewares.use(async (req, res, next) => {
|
|
146
|
+
const parsed = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
147
|
+
if (!parsed.pathname.startsWith(PREFIX)) return next();
|
|
148
|
+
// 数据根惰性读取:切换数据空间后无需重启
|
|
149
|
+
const root = getDataRoot();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const method = req.method || 'GET';
|
|
153
|
+
const itemId = parsed.pathname.slice(PREFIX.length).replace(/^\//, '');
|
|
154
|
+
|
|
155
|
+
if (method === 'GET' && parsed.searchParams.get('health') === '1') {
|
|
156
|
+
return sendJson(res, 200, { ok: true, file: storeFile(root) });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (method === 'GET' && !itemId) {
|
|
160
|
+
return sendJson(res, 200, { ok: true, ...readStore(root) });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (method === 'POST') {
|
|
164
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
165
|
+
const rawItems = Array.isArray(body.items) ? body.items : [body];
|
|
166
|
+
const meta = { mode: body.mode === 'ai' ? 'ai' : 'manual', sessionId: body.sessionId };
|
|
167
|
+
const store = readStore(root);
|
|
168
|
+
const created = [];
|
|
169
|
+
for (const raw of rawItems) {
|
|
170
|
+
const item = buildItem(raw, meta);
|
|
171
|
+
if (!item.title) {
|
|
172
|
+
return sendJson(res, 400, { ok: false, error: '任务名称不能为空' });
|
|
173
|
+
}
|
|
174
|
+
created.push(item);
|
|
175
|
+
}
|
|
176
|
+
store.items.push(...created);
|
|
177
|
+
writeStore(root, store);
|
|
178
|
+
return sendJson(res, 200, { ok: true, items: created, total: store.items.length });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (method === 'PUT') {
|
|
182
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
183
|
+
const id = text(body.id ?? itemId, 64);
|
|
184
|
+
if (!id) return sendJson(res, 400, { ok: false, error: '缺少任务 id' });
|
|
185
|
+
const store = readStore(root);
|
|
186
|
+
const idx = store.items.findIndex((x) => x.id === id);
|
|
187
|
+
if (idx < 0) return sendJson(res, 404, { ok: false, error: '任务不存在' });
|
|
188
|
+
|
|
189
|
+
const base = store.items[idx];
|
|
190
|
+
const patch = normalizePatch(body, base);
|
|
191
|
+
if ('title' in patch && !patch.title) {
|
|
192
|
+
return sendJson(res, 400, { ok: false, error: '任务名称不能为空' });
|
|
193
|
+
}
|
|
194
|
+
const next = { ...base, ...patch, updatedAt: new Date().toISOString() };
|
|
195
|
+
if (next.status === 'done' && base.status !== 'done') next.doneAt = next.updatedAt;
|
|
196
|
+
if (next.status !== 'done') next.doneAt = null;
|
|
197
|
+
store.items[idx] = next;
|
|
198
|
+
writeStore(root, store);
|
|
199
|
+
return sendJson(res, 200, { ok: true, item: next });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (method === 'DELETE') {
|
|
203
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
204
|
+
const ids = (Array.isArray(body.ids) ? body.ids : [body.id ?? itemId])
|
|
205
|
+
.map((x) => text(x, 64))
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
if (!ids.length) return sendJson(res, 400, { ok: false, error: '缺少任务 id' });
|
|
208
|
+
const store = readStore(root);
|
|
209
|
+
const drop = new Set(ids);
|
|
210
|
+
const before = store.items.length;
|
|
211
|
+
store.items = store.items.filter((x) => !drop.has(x.id));
|
|
212
|
+
writeStore(root, store);
|
|
213
|
+
return sendJson(res, 200, { ok: true, removed: before - store.items.length });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return sendJson(res, 405, { ok: false, error: 'Method Not Allowed' });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return sendJson(res, 500, { ok: false, error: String(err?.message ?? err) });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|