dsh-plugin-taskflow 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +244 -2
- package/client/client.js +1481 -0
- package/cordis.patch.yml +8 -0
- package/lib/index.js +1421 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,245 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-taskflow · DSH 任务流
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 给 DeepSeek Harness 的**任务清单 + 分段小结**插件:输入框上方一条液态玻璃清单,进行中的行带流动彩虹描边;每项任务完成时由模型写一句自己的小结;每个回合结束自动压成「一行标题 + 要点」。
|
|
4
|
+
>
|
|
5
|
+
> A task list and per-turn digest plugin for DeepSeek Harness: a liquid-glass task panel above the composer, a one-line model-written summary per task, and a condensed digest for every finished turn.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 从 npm 安装(发布后)
|
|
13
|
+
dsh plugin --profile web add dsh-plugin-taskflow
|
|
14
|
+
|
|
15
|
+
# 或直接从 GitHub 源码安装
|
|
16
|
+
dsh plugin --profile web add github:wk-xjd/dsh-plugin-taskflow
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
安装后**刷新页面**(桌面客户端:开发者工具里执行 `location.reload()`,或重启 App);
|
|
20
|
+
宿主半部更新需重启一次 App。
|
|
21
|
+
|
|
22
|
+
<details>
|
|
23
|
+
<summary>本地开发安装 / 源码目录方式</summary>
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/wk-xjd/dsh-plugin-taskflow.git
|
|
27
|
+
cd dsh-plugin-taskflow
|
|
28
|
+
node install.mjs # 同步到 ~/.dsh/plugins 并写入 profile 行
|
|
29
|
+
node install.mjs --rotate # 宿主代码热更(换路径,免重启)
|
|
30
|
+
node install.mjs --profile web # 装到别的 profile
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
</details>
|
|
34
|
+
|
|
35
|
+
## 它做了什么
|
|
36
|
+
|
|
37
|
+
给 DeepSeek Harness(Web GUI)补两块能力:
|
|
38
|
+
|
|
39
|
+
0. **对话内的任务清单**:替换内置的一行式 todo 行,把 agent 的 `todo_write` 渲染成 Claude 风格的清单卡片(`7 个任务 · 完成 6 · 待办 1` + `✔/◐/□` 逐项),位置就在那次工具调用处。
|
|
40
|
+
1. **常驻任务列表**:替换 composer 上方自带的 todo 面板(遮蔽同一个 `todo` 槽位)。有任务时,清单**一直显示**在输入框上方,宽度与输入框对齐(在 composer 包裹层里找到"最深的宽元素"= 输入卡片,复制它的 `width` 与左边缘,并用 `ResizeObserver` 跟随侧栏/窗口变化),液态玻璃背景(`backdrop-filter: blur(34px) saturate(1.7)` + 顶部渐变高光 + 内外描边);执行中的任务行带 Apple Intelligence 风格的彩虹描边与呼吸圆点;进度环是 SVG,按完成比例填充,最多 190px 高,超出滚动。
|
|
41
|
+
- 每个任务完成时,宿主用当前模型给它写**一句自己的小结**(≤60 字,基于该任务期间的工具调用与文件);
|
|
42
|
+
- 点**有小结的某一项**:弹出详情浮窗 —— 先是小结正文,再是开始/完成/用时、期间动作、涉及文件;
|
|
43
|
+
- **没有小结的项不可点击**,行尾显示「小结生成中…」或「无小结」;
|
|
44
|
+
- 点**进度环**:打开右侧栏,看**全部**分段小结;
|
|
45
|
+
- 折叠按钮:收成一行,默认展开;
|
|
46
|
+
- 回合结束仍处 `in_progress` 的任务不再假装在跑:宿主打 `pausedAt`,界面变琥珀色 `⏸`、停掉彩虹灯带、计时冻结,标题显示「等待确认:<该任务>」;
|
|
47
|
+
- 但如果这轮派出了**仍在运行的子 agent**(宿主从子会话 header 的 `origin=subagent`/`parentSession` 认出来),则该任务显示靛蓝色「子 agent 执行中:<该任务>」,计时继续走——不被误判成等待人工;
|
|
48
|
+
- 没有任务时回退成单行胶囊,显示最新一段小结标题。
|
|
49
|
+
2. **分段小结**:每个 assistant 回合结束时,宿主用**当前会话的路由**发一次很短的独立请求,把这一回合压缩成「一行标题 + 1–3 条要点」,然后
|
|
50
|
+
- 作为胶囊的一行标题展示,点开浮层看全部段落与要点;
|
|
51
|
+
- 通过插件自己的 HTTP 路由提供给浏览器。
|
|
52
|
+
|
|
53
|
+
宿主同时记录每个 todo 的 `startedAt / completedAt`,所以任务条能显示「这一项已经跑了 42s」。
|
|
54
|
+
|
|
55
|
+
## 目录
|
|
56
|
+
|
|
57
|
+
- [安装](#安装)
|
|
58
|
+
- [使用](#使用)
|
|
59
|
+
- [配置](#配置)
|
|
60
|
+
- [实现](#实现)
|
|
61
|
+
- [自查与排错](#自查与排错)
|
|
62
|
+
- [已知限制](#已知限制)
|
|
63
|
+
- [测试](#测试)
|
|
64
|
+
|
|
65
|
+
## 安装细节
|
|
66
|
+
|
|
67
|
+
本插件是**树外单插件**:不进 `dsh.profile.bundles`,只在目标 profile 的 `cordis.patch.yml` 里插一行
|
|
68
|
+
(`dsh plugin add` 会自动完成这件事)。
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
- insert:
|
|
72
|
+
- id: dsh-plugin-taskflow
|
|
73
|
+
name: 'dsh-plugin-taskflow'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 生效方式
|
|
77
|
+
|
|
78
|
+
| 改了什么 | 怎么生效 |
|
|
79
|
+
|---|---|
|
|
80
|
+
| 浏览器半部(`client/client.js`) | 刷新页面(桌面客户端可用开发者工具执行 `location.reload()`) |
|
|
81
|
+
| 宿主半部(`lib/index.js`) | 重启 App;本地开发可用 `node install.mjs --rotate` 换新路径立即生效 |
|
|
82
|
+
| patch 行本身 | `patchReload: live` 会监听;改同一行的 `name` 要先删后加,避免新旧实例抢同一张 HTTP 路由表 |
|
|
83
|
+
|
|
84
|
+
## 使用
|
|
85
|
+
|
|
86
|
+
| 位置 | 看到什么 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| 对话里每次 `todo_write` | 清单卡片:`5 个任务 · 完成 2 · 待办 3`,逐项 `✔` / `◐` / `□` |
|
|
89
|
+
| composer 上方 | **常驻清单**:`◔ 4/6 当前任务` + 逐项 `✔/◐/□`(最多 190px,超出滚动) |
|
|
90
|
+
| 点有小结的项 | 详情浮窗:任务小结正文 + 开始/完成/用时 + 期间动作 + 涉及文件 |
|
|
91
|
+
| 无小结的项 | 不可点击,行尾提示「小结生成中…」或「无小结」 |
|
|
92
|
+
| 点进度环 | 右侧栏「任务流」:全部 N 段小结 + 「整理最近一段」 |
|
|
93
|
+
| 无任务时 | 单行胶囊:`▤ 最新小结标题`(≤20 字),点击同样开侧栏 |
|
|
94
|
+
|
|
95
|
+
斜杠命令 `/taskflow`:立即整理最近一个还没有小结的回合。
|
|
96
|
+
|
|
97
|
+
## 配置
|
|
98
|
+
|
|
99
|
+
写在 patch 行的 `config:` 上(全部可选):
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
- insert:
|
|
103
|
+
- id: dsh-plugin-taskflow
|
|
104
|
+
name: '.../lib/index.js'
|
|
105
|
+
config:
|
|
106
|
+
enabled: true
|
|
107
|
+
minToolCalls: 1
|
|
108
|
+
minAssistantChars: 240
|
|
109
|
+
maxOutputTokens: 320
|
|
110
|
+
provider: '' # 留空 = 跟随会话最近一次请求的路由
|
|
111
|
+
model: ''
|
|
112
|
+
includeSubagents: false
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| 字段 | 默认 | 含义 |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `enabled` | `true` | `false` 时不调用模型,只出规则摘要 |
|
|
118
|
+
| `minToolCalls` | `1` | 工具调用数达到它才值得总结 |
|
|
119
|
+
| `minAssistantChars` | `240` | 或助手文本达到它 |
|
|
120
|
+
| `maxDigestChars` | `14000` | 递给模型的原文字符上限 |
|
|
121
|
+
| `maxOutputTokens` | `600` | 摘要输出上限;撞上限但有正文时照用并标 `truncated` |
|
|
122
|
+
| `timeoutMs` | `45000` | 单次调用看门狗 |
|
|
123
|
+
| `maxConcurrent` | `2` | 全局并发摘要数 |
|
|
124
|
+
| `provider` / `model` | 空 | 显式指定摘要路由;空则跟随会话 |
|
|
125
|
+
| `maxSegments` | `60` | 每会话保留的段数 |
|
|
126
|
+
| `includeSubagents` | `false` | 默认不给子 agent 会话生成小结(它们自带 turn 流,容易刷屏与烧钱) |
|
|
127
|
+
| `taskListPolicy` | `true` | 往系统提示注册「任务清单规范」段:无论任务大小先拆解、每条中文≤35 词、只留一个进行中、完成即勾 |
|
|
128
|
+
| `parallelPolicy` | `true` | 往系统提示注册「复杂任务:并行拆解」段(吸收 SDK `workflows/parallel-agents` SOP 的可执行部分) |
|
|
129
|
+
|
|
130
|
+
## 实现
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
lib/index.js 宿主半部:事件折叠、摘要、HTTP、斜杠命令(零 npm 依赖)
|
|
134
|
+
client/client.js 浏览器半部:预构建 classic script,注册三处 UI
|
|
135
|
+
install.mjs 同步到 ~/.dsh/plugins + 维护 patch 行 + 轮换热更
|
|
136
|
+
test/ 两半各自的离线测试(假 cordis 上下文 + React shim)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**任务清单规范(模型侧约束)**
|
|
140
|
+
|
|
141
|
+
插件向 `ctx.systemPrompt` 注册一段 `taskflow:task-list-policy`(order 9000),所有对话、所有 agent 都读得到:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
## 任务清单规范
|
|
145
|
+
- 无论任务大小(哪怕只改一行、只跑一条命令),都先用 todo_write 拆成可执行条目再动手;单步任务也给一条。
|
|
146
|
+
- 每条用中文书写,动词开头,最多 35 个词(中文约 60 字);一条只做一件事,但要把对象和判据写全。
|
|
147
|
+
- 写清对象(文件、命令、结论),不要用「处理一下」「继续优化」这类空话。
|
|
148
|
+
- 同一时间只保留一个 in_progress;做完一项立刻勾掉,不要攒到最后批量改。
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
关掉它:配置 `taskListPolicy: false`。
|
|
152
|
+
|
|
153
|
+
**复杂任务:并行拆解**(`parallelPolicy`,order 9010)
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
- 触发:出现 2 个以上可独立回答的未知,或 2 个以上可隔离的产物/模块。单文件单步不硬拆。
|
|
157
|
+
- 粒度:按「可独立消费的产物」切,每条写清产物、消费者与停止条件;不按文件数量机械切分。
|
|
158
|
+
- 执行:独立项用 subagent / workflow 并发派出(数量按宿主槽位减一),主代理同时推进
|
|
159
|
+
不依赖它们的关键路径;有依赖的串行交同一 agent 续作。
|
|
160
|
+
- 清单:并行项同时标 in_progress 并在文本前加「∥」;被依赖项保持 pending,解除后再置。
|
|
161
|
+
- 诚实:以真实工具调用作为派发证据;派发失败要保留该条并记录原因。
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
列表行按两行截断显示(`-webkit-line-clamp: 2`),35 词的条目也能读全;再长就以省略号收尾,完整文本在 tooltip 和详情浮窗里。
|
|
165
|
+
|
|
166
|
+
UI 配合:同时有 ≥2 项 `in_progress` 时,这些行的文本前会显示 `∥`,提示这是并行轨道。
|
|
167
|
+
|
|
168
|
+
**宿主**
|
|
169
|
+
|
|
170
|
+
- `ctx.on('session/event')` 跟踪 `turn/start`、`turn/end`、`todo/write`;子 agent 会话(`meta.origin === 'subagent'`)与**被中断/失败的回合**(`reason.kind` 为 `aborted`/`error`)默认跳过;
|
|
171
|
+
- 插件中途挂载也能找准回合起点:内存标记缺失时回扫日志里的 `turn/start`,不靠固定事件窗口;
|
|
172
|
+
- 回合结束时 `session.snapshotEvents(start, end + 1)` 取原文,折成「用户请求 / 工具调用 / 任务清单 / 助手收尾」;
|
|
173
|
+
- `ctx.llm.stream({ provider, model, system, messages, maxTokens, sessionId, signal })`,只累积 `text-delta`;`stop` 与 `max-tokens`(有正文时)都算成功,其余 finish 视为失败;
|
|
174
|
+
- 解析成 `{title, bullets}`,写入 `$DSH_HOME/taskflow/<session>.json`(400ms 防抖 + 原子 rename);
|
|
175
|
+
- `ctx.inject(['webServer'])` 注册 `/taskflow/state`、`/taskflow/summarize`、`/taskflow/health`、`/taskflow/diagnostics`;
|
|
176
|
+
- 没有 `llm` 服务、没有可用路由或调用失败时,退回规则摘要(工具数、文件、命令),UI 标记「规则摘要」。
|
|
177
|
+
|
|
178
|
+
**浏览器**
|
|
179
|
+
|
|
180
|
+
- `conversation.input.dock`(list,`id: 'todo'`,`priority: -1`)→ 任务条;lower priority 遮蔽自带面板(同 id 同 priority 会抛错);
|
|
181
|
+
- 胶囊只显示一行(≤20 字,超长省略);浮层用 `react-dom` 的 `createPortal` 挂到 `document.body`,定位锚在胶囊右上方,随滚动/缩放重算,点外部或 Esc 关闭;
|
|
182
|
+
- 说明:`conversation.chat.turnTail` 在 0.1.5-rc.2 的运行时实际是 **list** 槽(`ctx.slots.spec()` 返回 `{"kind":"list"}`),链式 `select` 注册会被拒(`list slot "..." requires options.id`),所以没有"回合末尾行内卡片"这种形态;
|
|
183
|
+
- `tool.call.toolview` 以 `key: 'todo_write'`、`priority: -1` 遮蔽内置的一行式 todo 行,换成清单卡片(同 key 同 priority 会抛错,低 priority 才生效);
|
|
184
|
+
- `conversation.input.dock` 承载常驻清单;宿主在每次 `tool/call` 时把它归到当前 `in_progress` 任务上(保留最近 12 条动作、10 个文件),详情浮窗才有"干了什么"可显示;
|
|
185
|
+
- 详情浮窗用 portal 挂在 `body`,锚在被点的行下方(空间不够翻到上方);右侧栏只注册一个 `taskflow` 标签页,由进度环触发,不占导航入口;
|
|
186
|
+
- 2.5s 轮询 `/taskflow/state`(页面隐藏时暂停),`fetch` 失败即静默降级。
|
|
187
|
+
|
|
188
|
+
**数据结构**(`~/.dsh/taskflow/<session>.json`):`segments[]`(turn、标题、要点、步数、文件、耗时、路由、usage)、
|
|
189
|
+
`tasks.items`(按任务文本记首次出现/开始/完成)、`tasks.history`、`lastTurn`、`skipped[]`。
|
|
190
|
+
|
|
191
|
+
## 自查与排错
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
curl -s http://127.0.0.1:<port>/taskflow/health # 在线状态 + 客户端握手 + 各面渲染尺寸
|
|
195
|
+
curl -s http://127.0.0.1:<port>/taskflow/diagnostics # 浏览器半部是否进图(clientPath / bundle URL)
|
|
196
|
+
curl -s "http://127.0.0.1:<port>/taskflow/diagnostics?llm=1" # 真发一次极小请求,验证摘要链路
|
|
197
|
+
curl -s -X POST http://127.0.0.1:<port>/taskflow/open-panel # 展开常驻清单(若被折叠)
|
|
198
|
+
curl -s -X POST http://127.0.0.1:<port>/taskflow/open-detail # 打开第一项的任务详情浮窗
|
|
199
|
+
curl -s -X POST http://127.0.0.1:<port>/taskflow/open-sidebar # 打开侧栏(全部小结)
|
|
200
|
+
curl -s -X POST -H 'content-type: application/json' \
|
|
201
|
+
-d '{"session":"<id>","turn":3,"force":true}' \
|
|
202
|
+
http://127.0.0.1:<port>/taskflow/summarize # 强制重做某段并返回成败原因
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`/taskflow/health` 里的关键字段:
|
|
206
|
+
|
|
207
|
+
| 字段 | 含义 |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `client` | 浏览器半部的握手(`build`、`href`、`UA`);为空说明页面还没加载插件 |
|
|
210
|
+
| `stateHits` / `lastStateAgoMs` | 客户端轮询次数与最近一次时间;不再增长说明界面没在跑 |
|
|
211
|
+
| `renders.<part>` | 各面实测几何(`visible`、`width`/`height`):`taskcard`(对话内卡片)/`dock`(常驻清单)/`detail`(任务详情浮窗)/`sidebar`(全部小结)/`apply` |
|
|
212
|
+
| `lastSummary` | 最近一次摘要的路由、失败原因与是否规则摘要 |
|
|
213
|
+
|
|
214
|
+
`/taskflow/summarize` 的返回里 `failure` 会直接给出原因(例如 `... finished with max-tokens`、`no provider/model route`),排查不用翻日志。
|
|
215
|
+
|
|
216
|
+
- `?llm=1` 返回 `{"ok":true,"route":{...},"text":"…"}` 说明路由、消息形状与流式解析都通。
|
|
217
|
+
- 小结是「规则摘要」而不是模型输出,说明 `llm` 服务缺失、没有路由或调用失败——用 `?llm=1` 看具体错误。
|
|
218
|
+
- 浏览器里没有 UI:先确认 `diagnostics` 里 `entry` 非空,再刷新页面;仍无则看 console 的 `slot entry crashed in '<slot>'`。
|
|
219
|
+
|
|
220
|
+
## 兼容与权限
|
|
221
|
+
|
|
222
|
+
- **DSH 版本**:面向 `dsh 0.1.5-rc.1`(客户端包 `0.1.5-rc.2`)实测,桌面客户端 0.2.2-rc.1;更低版本未验证。
|
|
223
|
+
- **平台**:Web profile 与桌面客户端(Electron)都验证过;纯 CLI/headless profile 没有 UI,只会有宿主半部。
|
|
224
|
+
- **依赖**:宿主半部零 npm 依赖(只用 `node:fs` / `node:path` / `node:os` / `node:crypto`);客户端半部只 `require('react')` 与 `require('react-dom')`(DSH 内置种子模块)。
|
|
225
|
+
- **数据落地**:`$DSH_HOME/taskflow/<session>.json`,只存任务、耗时与小结文本;删除它不损失会话正确性。
|
|
226
|
+
- **外部调用**:除"当前会话模型"外不访问任何网络服务;摘要调用走宿主已配置的 provider 路由(默认每次回合 1 次、每个完成的任务 1 次)。
|
|
227
|
+
- **HTTP 路由**:仅注册在宿主本机 web server(`/taskflow/state`、`/taskflow/summarize`、`/taskflow/health` 等),只读本机会话数据。
|
|
228
|
+
- **系统提示**:注册两段约束(`taskflow:task-list-policy`、`taskflow:parallel-policy`)与一条运行时提醒,可用配置关闭。
|
|
229
|
+
|
|
230
|
+
## 已知限制
|
|
231
|
+
|
|
232
|
+
- **没有行内(回合末尾)卡片**:`conversation.chat.turnTail` 在本版本运行时是 list 槽,链式注册被拒;小结出现在胶囊标题与浮层列表里。
|
|
233
|
+
- **手动整理需要会话仍活着**:`ctx.get('sessions').get(id)` 拿不到时只能看已有结果。
|
|
234
|
+
- **摘要是一次额外模型调用**:默认每回合最多一次,受 `minToolCalls` / `minAssistantChars` 节流;不想要就 `enabled: false`。
|
|
235
|
+
- **HTTP 取数依赖 web server**:非 web 表面下任务条仍可用,但耗时与小结不可用。
|
|
236
|
+
- **宿主半部改动必须重启**(或用 `--rotate`):这是 Loader 的 ESM 缓存行为,不是插件缺陷。
|
|
237
|
+
|
|
238
|
+
## 测试
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
node test/host.test.mjs # 事件 → 摘要 → HTTP 全链路(假 LLM、假 ctx)
|
|
242
|
+
node test/client.test.mjs # 注册、selector、三个组件渲染(React shim)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
两个测试都不需要 DSH 进程,也不联网。
|