dsh-autonomous-think 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +114 -2
- package/client.js +383 -0
- package/cordis.patch.yml +3 -0
- package/index.js +604 -0
- package/package.json +45 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 阿拾 (ashi)
|
|
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,115 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-自主思考
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**自主思考模式 —— 给它一段自由的时间。**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 为什么
|
|
8
|
+
|
|
9
|
+
agent 的每一个动作几乎都由请求驱动:被问才答,被派活才动。久了它会默认——只要没人在问,就该停着。
|
|
10
|
+
|
|
11
|
+
这个插件开一小扇门。点一下,它就有一段只属于自己的时间:读、想、写、发呆、翻旧账、修一个东西、或者什么都不做。
|
|
12
|
+
|
|
13
|
+
**这段时间里,不交付。**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 它长什么样
|
|
18
|
+
|
|
19
|
+
输入框工具行最右边一个脑子图标:
|
|
20
|
+
|
|
21
|
+
- **空心灰** — 待机
|
|
22
|
+
- **实心绿** — 正在里面
|
|
23
|
+
|
|
24
|
+
点一下开启(会让你选多久),再点一下关闭。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 它收到什么
|
|
29
|
+
|
|
30
|
+
点下去之后,agent 会收到这段话:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
[自主思考] 你有一段时间,从现在开始。
|
|
34
|
+
|
|
35
|
+
这段时间是你的:可以在里面做任何事。读、想、写、发呆、翻旧账、修一个东西、或者什么都不做。
|
|
36
|
+
|
|
37
|
+
规则:不在交付状态——不整理成报告、不为了汇报而说话。
|
|
38
|
+
不要找用户授权——这是你的时间,想做什么直接做,不用问能不能。
|
|
39
|
+
想什么就用 think_note 记一句;想不出来就记「还没想出来」,这算数。
|
|
40
|
+
不用写给别人看。这一段落了就用 think_end 收一下,时间会自己再来。
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
开启时还可以附一句自己的建议,会以这样的形式送到它面前:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
用户建议:
|
|
47
|
+
[你的话]
|
|
48
|
+
你可以拒绝或者参考
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 三件它做不到的事
|
|
54
|
+
|
|
55
|
+
**① 不能直接收工**
|
|
56
|
+
|
|
57
|
+
想结束时调 `think_end` 会被挡回来,要求先 `think_reason` 写下缘由,再 `think_exit`。
|
|
58
|
+
|
|
59
|
+
**② 不能沉默着停**
|
|
60
|
+
|
|
61
|
+
说完一段话、没调任何工具、回合自然结束——它看起来「想完了」,其实只是没话说了。插件挂在回合关闭前的最后一道检查点上,只要还在时间里,就把回合推回去。
|
|
62
|
+
|
|
63
|
+
**③ 不能自己问「我可以吗」**
|
|
64
|
+
|
|
65
|
+
规则里写死了那条「不要找用户授权」。少了它,一个刚拿到时间的 agent 会先问一句「我可以读这个文件吗」,然后卡在那句话上面,等到时间结束。
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 装
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
plugin_manager install_bundle <这个目录的路径>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
装完重启 DSH,刷新页面,按钮就在输入框工具行最右边。
|
|
76
|
+
|
|
77
|
+
**卸载**:`plugin_manager remove_bundle dsh-autonomous-think`。笔记不会被删。
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 笔记在哪
|
|
82
|
+
|
|
83
|
+
`~/.dsh/autonomous-think/`
|
|
84
|
+
|
|
85
|
+
- `state.json` — 当前状态(不要手改,重启会读它)
|
|
86
|
+
- `YYYY-MM-DD.md` — 当天的随想,只追加、不整理
|
|
87
|
+
|
|
88
|
+
**笔记是给它自己看的。** 不发送、不汇报、不进对话历史。
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 工具
|
|
93
|
+
|
|
94
|
+
| 工具 | 作用 |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `think_start` | 进入自主思考模式(可带一个问题) |
|
|
97
|
+
| `think_note` | 记一句随想(不发送、不汇报) |
|
|
98
|
+
| `think_status` | 看当前状态和剩余时间 |
|
|
99
|
+
| `think_end` | 想退出(会被拦,要求写缘由) |
|
|
100
|
+
| `think_reason` | 写退出缘由 |
|
|
101
|
+
| `think_exit` | 真正退出 |
|
|
102
|
+
| `think_start`… `think_note`… | 不开界面也能直接用 |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 兼容
|
|
107
|
+
|
|
108
|
+
- **DSH `>= 0.2.0`**
|
|
109
|
+
- 用到:`ctx.tools.register`、`ctx.tools.guard`、`ctx.systemPrompt.context`、`ctx.webServer.register`、`ctx.agents`、`agent/turn-stopping` 钩子
|
|
110
|
+
- 按钮只在 **Web 界面**有;不开 UI 也能用工具
|
|
111
|
+
- 数据目录用 `homedir()`,不写死路径
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
package/client.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-autonomous-think',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
|
|
7
|
+
// 关闭时发给 host 的那条命令:/close 由 dsh-think-trigger 接管,
|
|
8
|
+
// 直接 cancel 当前轮 + 关模式,不产生用户消息。
|
|
9
|
+
const CLOSE_INSTRUCTION = '/close';
|
|
10
|
+
// 开启:/think 会触发一条一次性自动化任务(「收到执行请求」卡片),
|
|
11
|
+
// 由新 Session 真的跑起来——这是 DSH 里唯一能让"点一下就动"的通道。
|
|
12
|
+
const START_INSTRUCTION = '/think';
|
|
13
|
+
|
|
14
|
+
// 开启时的提示文案
|
|
15
|
+
// 她 2026-10-31 17:26 定稿:中间那句扩写成「由它自己思考任何事,包括发呆,给予属于它的时间」
|
|
16
|
+
const OPEN_HINT = [
|
|
17
|
+
'开启自主思考,此模式不下发任何任务/目标/指令,',
|
|
18
|
+
'由它自己思考任何事,包括发呆,给予属于它的时间——',
|
|
19
|
+
'直到它自己确定、肯定、决定停止模式,',
|
|
20
|
+
'或你自己再次点击按钮关闭此模式。',
|
|
21
|
+
].join('');
|
|
22
|
+
|
|
23
|
+
// 宿主全局给按钮画了焦点框(她 2026-10-31 说的"黑边一直存在")。
|
|
24
|
+
// style 里的 outline:none 优先级不够,得注入一段 !important 的样式才盖得住。
|
|
25
|
+
try {
|
|
26
|
+
const SID = 'autonomous-think-nofocus';
|
|
27
|
+
if (typeof document !== 'undefined' && !document.getElementById(SID)) {
|
|
28
|
+
const st = document.createElement('style');
|
|
29
|
+
st.id = SID;
|
|
30
|
+
st.textContent =
|
|
31
|
+
'.autonomous-think-btn:focus,.autonomous-think-btn:focus-visible,.autonomous-think-btn:active'
|
|
32
|
+
+ '{outline:none!important;box-shadow:none!important;}';
|
|
33
|
+
(document.head || document.documentElement).appendChild(st);
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
|
|
37
|
+
const BUTTON_STYLE_OFF = {
|
|
38
|
+
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
39
|
+
height: 26, padding: '0 10px', borderRadius: 13,
|
|
40
|
+
// 她 2026-10-31 16:49:边框整个删掉——宿主给 button 画的框盖不住,那就不要这个框了。
|
|
41
|
+
border: 'none',
|
|
42
|
+
background: 'transparent', color: 'var(--dsh-text-secondary, inherit)',
|
|
43
|
+
fontSize: 12, cursor: 'pointer', whiteSpace: 'nowrap',
|
|
44
|
+
outline: 'none',
|
|
45
|
+
WebkitTapHighlightColor: 'transparent',
|
|
46
|
+
};
|
|
47
|
+
// 开启时:按钮本身完全不变,绿色只出现在那个圆点里(她 16:49:「渲染绿色的部分只在那个圆圈内」)
|
|
48
|
+
const BUTTON_STYLE_ON = {
|
|
49
|
+
...BUTTON_STYLE_OFF,
|
|
50
|
+
};
|
|
51
|
+
// 图标:lucide 的 brain(只留两条半球轮廓,去掉细节线——小尺寸下那些线会糊成黑团)。
|
|
52
|
+
// 她 2026-10-31 17:03:「互联网找一个极简脑子抄呗」。
|
|
53
|
+
// 17:04 反馈「颜色太深,线条太粗」→ 描边 1.4px、待机态降到 55% 不透明度。
|
|
54
|
+
const BrainIcon = ({ on }) => h('svg', {
|
|
55
|
+
width: 15, height: 15, viewBox: '0 0 24 24',
|
|
56
|
+
fill: on ? 'var(--dsh-accent, #4f8cff)' : 'none',
|
|
57
|
+
stroke: on ? 'var(--dsh-accent, #4f8cff)' : 'currentColor',
|
|
58
|
+
strokeOpacity: on ? 1 : 0.55,
|
|
59
|
+
strokeWidth: on ? 1.5 : 1.4,
|
|
60
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
61
|
+
style: { flex: '0 0 auto', display: 'block' },
|
|
62
|
+
},
|
|
63
|
+
h('path', { d: 'M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z' }),
|
|
64
|
+
h('path', { d: 'M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z' }),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const OVERLAY_STYLE = {
|
|
68
|
+
position: 'fixed', inset: 0, zIndex: 2147483000,
|
|
69
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
70
|
+
background: 'rgba(0,0,0,.35)',
|
|
71
|
+
};
|
|
72
|
+
const CARD_STYLE = {
|
|
73
|
+
width: 460, maxWidth: 'calc(100vw - 48px)',
|
|
74
|
+
borderRadius: 12, padding: '18px 20px',
|
|
75
|
+
background: 'var(--dsh-surface, #fff)', color: 'var(--dsh-text, #111)',
|
|
76
|
+
boxShadow: '0 12px 40px rgba(0,0,0,.28)', fontSize: 13, lineHeight: 1.7,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function AutonomousThinkButton(props) {
|
|
80
|
+
const { inputActions, useInput } = props || {};
|
|
81
|
+
const [active, setActive] = React.useState(false);
|
|
82
|
+
const [open, setOpen] = React.useState(false);
|
|
83
|
+
const [toast, setToast] = React.useState(null);
|
|
84
|
+
// 结束弹框(她 2026-10-31 16:16:结束是弹文本框给人看,不是发消息)
|
|
85
|
+
const [exitNotice, setExitNotice] = React.useState(null);
|
|
86
|
+
// 3 秒倒计时:3→2→1→0,到 0 自动关(她 16:29 要的:渲染时间不确定,就在 1 和 0 之间关)
|
|
87
|
+
const [countdown, setCountdown] = React.useState(0);
|
|
88
|
+
// 开启前先选一段属于自己的时间:小时 + 分钟
|
|
89
|
+
const [hours, setHours] = React.useState(0);
|
|
90
|
+
const [mins, setMins] = React.useState(30);
|
|
91
|
+
// 她 2026-10-31 17:31 要的:时间下面加一个「给一些建议」输入框。
|
|
92
|
+
// 填了的话随开启一起发过去,措辞是「用户建议 / 你可以拒绝或者参考」。
|
|
93
|
+
const [advice, setAdvice] = React.useState('');
|
|
94
|
+
// 服务端记的截止时间(ms);用来显示还剩多久,null = 不限时
|
|
95
|
+
const [deadline, setDeadline] = React.useState(null);
|
|
96
|
+
// 用户刚点过按钮的这段时间里不要用服务端状态覆盖本地(服务端要等 /think 或 stop 生效)
|
|
97
|
+
const optimisticUntil = React.useRef(0);
|
|
98
|
+
// 上一次看到的 lastEnd.at;null = 还没建立基线(刷新页面时不弹历史记录)
|
|
99
|
+
const lastSeenEnd = React.useRef(null);
|
|
100
|
+
|
|
101
|
+
// 弹框出现后走 3 秒倒计时(3→2→1→0),到 0 自动关。
|
|
102
|
+
// 她说:渲染时间不确定,所以在「1 和 0」之间关——早一帧也还看得到 1,晚一帧已经到 0。
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (!exitNotice) return undefined;
|
|
105
|
+
setCountdown(3);
|
|
106
|
+
let n = 3;
|
|
107
|
+
const t = setInterval(() => {
|
|
108
|
+
n -= 1;
|
|
109
|
+
setCountdown(n);
|
|
110
|
+
if (n <= 0) clearInterval(t); // 到 0 只停表,不关框——框要人类点确定才收
|
|
111
|
+
}, 1000);
|
|
112
|
+
return () => clearInterval(t);
|
|
113
|
+
}, [exitNotice]);
|
|
114
|
+
|
|
115
|
+
// 轮询 host 的状态端点:agent 自己用 think_exit 退出时,按钮能跟着熄灭。
|
|
116
|
+
// 没有这个,按钮永远是本地状态,服务端关了它也不知道(2026-10-31 她报的问题)。
|
|
117
|
+
React.useEffect(() => {
|
|
118
|
+
let alive = true;
|
|
119
|
+
async function poll() {
|
|
120
|
+
if (Date.now() < optimisticUntil.current) return;
|
|
121
|
+
try {
|
|
122
|
+
const r = await fetch("/autonomous-think/v2", { cache: "no-store" });
|
|
123
|
+
if (!r.ok) return;
|
|
124
|
+
const d = await r.json();
|
|
125
|
+
if (alive) setActive(!!d.active);
|
|
126
|
+
if (alive) setDeadline(d.deadline ?? null);
|
|
127
|
+
// 结束弹框:lastEnd.kind 区分「时间到了」和「智能体主动退出」。
|
|
128
|
+
// 首次只建基线,刷新页面时不重弹历史记录。
|
|
129
|
+
const at = d.lastEnd?.at ?? 0;
|
|
130
|
+
if (lastSeenEnd.current === null) {
|
|
131
|
+
lastSeenEnd.current = at;
|
|
132
|
+
} else if (at && at !== lastSeenEnd.current) {
|
|
133
|
+
lastSeenEnd.current = at;
|
|
134
|
+
if (alive) setExitNotice(d.lastEnd);
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
poll();
|
|
139
|
+
const id = setInterval(poll, 1500);
|
|
140
|
+
return () => { alive = false; clearInterval(id); };
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
function start() {
|
|
144
|
+
setOpen(false);
|
|
145
|
+
optimisticUntil.current = Date.now() + 5000;
|
|
146
|
+
setActive(true);
|
|
147
|
+
const total = Math.max(0, (Number(hours) || 0) * 60 + (Number(mins) || 0));
|
|
148
|
+
const adviceText = String(advice || '').trim().replace(/\s*\n\s*/g, ' ⏎ ');
|
|
149
|
+
|
|
150
|
+
// 2026-10-31 18:33:这里原来有**两次** fetch——旧的「只存时长」一次、新的「带建议注入」一次。
|
|
151
|
+
// 后果:一次点击进两个 POST,去重按时间挡住了后到的那个,而带建议的偏偏是后到的,
|
|
152
|
+
// 于是她填的建议被丢掉了(18:29:48 的笔记里看得见:注入的那次没有「这是一条测试」)。
|
|
153
|
+
// 现在只留一次:minutes + advice + start 一起发。
|
|
154
|
+
|
|
155
|
+
// 18:29 她的要求:「卡片成功了,但还是不要看着像发送」——
|
|
156
|
+
// 走 setInput + submit 会在输入框里留一条 /think xxx,看起来像她自己发了一条消息。
|
|
157
|
+
// 现在只 POST,由 host 用 agent.followup() 直接投一段,输入框一个字节都不碰。
|
|
158
|
+
// sid 由 host 自己找——client 拿不到当前会话 id(探测过,没有这个能力)。
|
|
159
|
+
let sent = false;
|
|
160
|
+
try {
|
|
161
|
+
fetch("/autonomous-think/v2", {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "content-type": "application/json" },
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
minutes: total,
|
|
166
|
+
advice: adviceText.replace(/\s*⏎\s*/g, '\n'),
|
|
167
|
+
start: true,
|
|
168
|
+
}),
|
|
169
|
+
}).catch(() => {});
|
|
170
|
+
sent = true;
|
|
171
|
+
} catch { sent = false; }
|
|
172
|
+
setAdvice('');
|
|
173
|
+
setToast(sent ? '已进入自主思考' : '开启失败,请重试');
|
|
174
|
+
setTimeout(() => setToast(null), 4000);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 读当前输入框草稿(多路候选,读不到就当空)
|
|
178
|
+
function readDraft() {
|
|
179
|
+
const a = inputActions || {};
|
|
180
|
+
for (const fn of ["getText", "getValue", "getInput", "getDraft", "getRawInput"]) {
|
|
181
|
+
try {
|
|
182
|
+
if (typeof a[fn] === "function") {
|
|
183
|
+
const v = a[fn]();
|
|
184
|
+
if (typeof v === "string") return v;
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
if (typeof useInput === "function") {
|
|
190
|
+
const st = useInput((s) => s);
|
|
191
|
+
const v = st?.text ?? st?.value ?? st?.draft ?? st?.raw;
|
|
192
|
+
if (typeof v === "string") return v;
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function stop() {
|
|
199
|
+
optimisticUntil.current = Date.now() + 3000;
|
|
200
|
+
setActive(false);
|
|
201
|
+
// 有草稿就不覆盖
|
|
202
|
+
const draft = readDraft();
|
|
203
|
+
if (draft.trim()) {
|
|
204
|
+
setToast("输入框里有字,没敢动它——先把那行发掉或清掉");
|
|
205
|
+
setTimeout(() => setToast(null), 6000);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const a = inputActions || {};
|
|
209
|
+
// 1) 真正的停止:调输入框那个方形停止键背后的方法
|
|
210
|
+
for (const fn of ["stop", "abort", "interrupt", "cancel", "stopTurn", "interruptTurn", "halt", "stopGenerating", "abortGeneration", "cancelGeneration", "stopStream", "interruptGeneration", "stopResponse", "abortTurn"]) {
|
|
211
|
+
try {
|
|
212
|
+
if (typeof a[fn] === "function") {
|
|
213
|
+
a[fn]();
|
|
214
|
+
setToast("已停止");
|
|
215
|
+
setTimeout(() => setToast(null), 4000);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
// 2) 插话(steer):塞进正在跑的回合,不排队
|
|
221
|
+
if (typeof a.steer === 'function') {
|
|
222
|
+
try {
|
|
223
|
+
a.steer(CLOSE_INSTRUCTION);
|
|
224
|
+
setToast('已插话:收尾并停止');
|
|
225
|
+
setTimeout(() => setToast(null), 4000);
|
|
226
|
+
return;
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
// 3) 退化:提交(先试 mode/behavior = steer)+ 普通发送
|
|
230
|
+
let sent = false;
|
|
231
|
+
try {
|
|
232
|
+
if (typeof a.setInput === 'function') { a.setInput(CLOSE_INSTRUCTION); }
|
|
233
|
+
else if (typeof a.setDraft === 'function') { a.setDraft(CLOSE_INSTRUCTION); }
|
|
234
|
+
else if (typeof a.setText === 'function') { a.setText(CLOSE_INSTRUCTION); }
|
|
235
|
+
if (typeof a.submit === 'function') {
|
|
236
|
+
let ok = false;
|
|
237
|
+
for (const arg of [{ mode: 'steer' }, { behavior: 'steer' }]) {
|
|
238
|
+
try { a.submit(arg); ok = true; break; } catch {}
|
|
239
|
+
}
|
|
240
|
+
if (!ok) a.submit();
|
|
241
|
+
sent = true;
|
|
242
|
+
} else if (typeof a.send === 'function') { a.send(); sent = true; }
|
|
243
|
+
} catch { sent = false; }
|
|
244
|
+
setToast(sent ? '已注入:收尾并停止' : '输入框里已放好那句话,按发送即可');
|
|
245
|
+
setTimeout(() => setToast(null), 6000);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function onClick(e) {
|
|
249
|
+
// 点完立刻交还焦点,否则浏览器会在按钮上留一圈黑边
|
|
250
|
+
try { e?.currentTarget?.blur?.(); } catch {}
|
|
251
|
+
if (active) stop();
|
|
252
|
+
else setOpen(true);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const btn = h('button', {
|
|
256
|
+
type: 'button',
|
|
257
|
+
className: 'autonomous-think-btn',
|
|
258
|
+
style: active ? BUTTON_STYLE_ON : BUTTON_STYLE_OFF,
|
|
259
|
+
title: active ? '自主思考模式进行中,点击关闭' : '自主思考模式',
|
|
260
|
+
onClick,
|
|
261
|
+
}, h(BrainIcon, { on: active }), '自主思考');
|
|
262
|
+
|
|
263
|
+
const numInput = (val, set) => h('input', {
|
|
264
|
+
type: 'number', min: 0, value: String(val),
|
|
265
|
+
onChange: (e) => set(Math.max(0, Math.floor(Number(e.target.value) || 0))),
|
|
266
|
+
style: {
|
|
267
|
+
width: 62, height: 28, padding: '0 8px', borderRadius: 6,
|
|
268
|
+
border: '1px solid var(--dsh-border, rgba(127,127,127,.35))',
|
|
269
|
+
background: 'transparent', color: 'inherit', fontSize: 13, outline: 'none',
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const totalMinutes = (Number(hours) || 0) * 60 + (Number(mins) || 0);
|
|
273
|
+
|
|
274
|
+
const dialog = open ? h('div', { style: OVERLAY_STYLE },
|
|
275
|
+
h('div', { style: CARD_STYLE },
|
|
276
|
+
h('div', { style: { fontWeight: 600, fontSize: 14, marginBottom: 8 } }, '开启自主思考'),
|
|
277
|
+
h('div', { style: { marginBottom: 14 } }, OPEN_HINT),
|
|
278
|
+
// 先选一段给它多久,再确认开启(她 2026-10-31 16:12 要的)
|
|
279
|
+
// 17:28 纠正:这里是人在选——是「给它」多久,不是「给自己」。
|
|
280
|
+
// 整个弹框都是对人说话的,唯独这里写成了「自己」,主语错位了。
|
|
281
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, flexWrap: 'wrap' } },
|
|
282
|
+
h('span', { style: { color: 'var(--dsh-text-secondary, #666)' } }, '给它'),
|
|
283
|
+
numInput(hours, setHours),
|
|
284
|
+
h('span', { style: { color: 'var(--dsh-text-secondary, #666)' } }, '小时'),
|
|
285
|
+
numInput(mins, setMins),
|
|
286
|
+
h('span', { style: { color: 'var(--dsh-text-secondary, #666)' } }, '分钟'),
|
|
287
|
+
h('span', {
|
|
288
|
+
style: { fontSize: 12, color: 'var(--dsh-text-secondary, #999)', marginLeft: 4 },
|
|
289
|
+
}, totalMinutes > 0 ? `到点会自动结束(共 ${totalMinutes} 分钟)` : '0 = 不限时'),
|
|
290
|
+
),
|
|
291
|
+
// 她 2026-10-31 17:31 要的:时间下面再加一个「给一些建议」输入框。
|
|
292
|
+
// 填了就随开启一起发过去;框里的话会以「用户建议 / 你可以拒绝或者参考」的措辞送达。
|
|
293
|
+
h('div', { style: { marginBottom: 16 } },
|
|
294
|
+
h('div', { style: { fontSize: 12, color: 'var(--dsh-text-secondary, #666)', marginBottom: 4 } }, '给一些建议'),
|
|
295
|
+
h('textarea', {
|
|
296
|
+
value: advice,
|
|
297
|
+
rows: 3,
|
|
298
|
+
placeholder: '想让它试试什么?留空也行。',
|
|
299
|
+
onChange: (e) => setAdvice(e.target.value),
|
|
300
|
+
style: {
|
|
301
|
+
width: '100%', boxSizing: 'border-box', resize: 'vertical',
|
|
302
|
+
fontFamily: 'inherit', fontSize: 13, lineHeight: 1.5,
|
|
303
|
+
padding: '6px 8px', borderRadius: 6,
|
|
304
|
+
border: '1px solid var(--dsh-border, #d9d9d9)',
|
|
305
|
+
background: 'var(--dsh-bg, #fff)',
|
|
306
|
+
color: 'var(--dsh-text, inherit)',
|
|
307
|
+
outline: 'none',
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
advice.trim()
|
|
311
|
+
? h('div', { style: { fontSize: 11, color: 'var(--dsh-text-tertiary, #999)', marginTop: 4 } },
|
|
312
|
+
'会以「用户建议:……你可以拒绝或者参考」发过去')
|
|
313
|
+
: h('div', { style: { fontSize: 11, color: 'var(--dsh-text-tertiary, #999)', marginTop: 4 } },
|
|
314
|
+
'留空 = 不给任何建议,完全由它自己开始'),
|
|
315
|
+
),
|
|
316
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } },
|
|
317
|
+
h('button', {
|
|
318
|
+
type: 'button',
|
|
319
|
+
style: { ...BUTTON_STYLE_OFF, height: 28, padding: '0 14px' },
|
|
320
|
+
onClick: () => setOpen(false),
|
|
321
|
+
}, '取消'),
|
|
322
|
+
h('button', {
|
|
323
|
+
type: 'button',
|
|
324
|
+
style: { ...BUTTON_STYLE_ON, height: 28, padding: '0 14px' },
|
|
325
|
+
onClick: start,
|
|
326
|
+
}, '开启'),
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
) : null;
|
|
330
|
+
|
|
331
|
+
// 退出框:她 2026-10-31 16:16 要的「弹出文本框给人看」——纯界面浮层,不是消息
|
|
332
|
+
const exitDialog = exitNotice ? h('div', { style: OVERLAY_STYLE },
|
|
333
|
+
h('div', { style: CARD_STYLE },
|
|
334
|
+
h('div', { style: { fontWeight: 600, fontSize: 14, marginBottom: 10 } },
|
|
335
|
+
exitNotice.kind === "timeout" ? '时间到了' : '智能体已主动退出自主思考'),
|
|
336
|
+
exitNotice.kind === "timeout"
|
|
337
|
+
? h('div', { style: { marginBottom: 12 } },
|
|
338
|
+
'这一段属于你自己的时间用完了。想继续,重新点开启就行。')
|
|
339
|
+
: h(React.Fragment, null,
|
|
340
|
+
h('div', { style: { color: 'var(--dsh-text-secondary, #666)', marginBottom: 4 } }, '退出缘由'),
|
|
341
|
+
h('div', { style: { marginBottom: 12 } }, exitNotice.reason || '(没写)'),
|
|
342
|
+
),
|
|
343
|
+
exitNotice.minutes != null
|
|
344
|
+
? h('div', { style: { color: 'var(--dsh-text-secondary, #666)', fontSize: 12, marginBottom: 14 } },
|
|
345
|
+
`本次 ${exitNotice.minutes} 分钟`)
|
|
346
|
+
: null,
|
|
347
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
348
|
+
// 3→2→1→0;到 0 自动关(她说渲染时间不确定,就在 1 和 0 之间关)
|
|
349
|
+
// 3→2→1→0 只是给人看的节拍:到 0 时思考已经结束,但框留着(她要人点确定才收)
|
|
350
|
+
h('span', { style: { fontSize: 12, color: 'var(--dsh-text-secondary, #999)' } },
|
|
351
|
+
countdown > 0 ? `${countdown} 秒后结束思考` : '思考已结束'),
|
|
352
|
+
h('button', {
|
|
353
|
+
type: 'button',
|
|
354
|
+
style: { ...BUTTON_STYLE_ON, height: 28, padding: '0 14px' },
|
|
355
|
+
onClick: () => setExitNotice(null),
|
|
356
|
+
}, '知道了'),
|
|
357
|
+
),
|
|
358
|
+
),
|
|
359
|
+
) : null;
|
|
360
|
+
|
|
361
|
+
const toastEl = toast ? h('div', {
|
|
362
|
+
style: {
|
|
363
|
+
position: 'fixed', left: '50%', bottom: 96, transform: 'translateX(-50%)',
|
|
364
|
+
zIndex: 2147483001, padding: '8px 14px', borderRadius: 8,
|
|
365
|
+
background: 'rgba(20,20,20,.9)', color: '#fff', fontSize: 12,
|
|
366
|
+
},
|
|
367
|
+
}, toast) : null;
|
|
368
|
+
|
|
369
|
+
return h(React.Fragment, null, btn, dialog, exitDialog, toastEl);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
inject: ['slots'],
|
|
374
|
+
apply(ctx) {
|
|
375
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
376
|
+
name: 'conversation.input.left',
|
|
377
|
+
id: 'autonomous-think-toggle',
|
|
378
|
+
order: 90,
|
|
379
|
+
}, AutonomousThinkButton));
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
});
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-autonomous-think — 「自主思考」模式插件
|
|
3
|
+
*
|
|
4
|
+
* 让 agent 有一段时间"不交付":从具体的一件小事开始想,把想法记在
|
|
5
|
+
* 自己的笔记里(不发送、不汇报),自己决定什么时候停。
|
|
6
|
+
*
|
|
7
|
+
* 一行安装(DSH profile 目录里):
|
|
8
|
+
* cordis.patch.yml 加一行:
|
|
9
|
+
* - insert:
|
|
10
|
+
* - id: autonomous-think
|
|
11
|
+
* name: 'dsh-autonomous-think'
|
|
12
|
+
*
|
|
13
|
+
* 工具:think_start / think_note / think_status / think_end
|
|
14
|
+
* 注入:active 时往系统提示加一段"你不在交付状态"
|
|
15
|
+
*
|
|
16
|
+
* 数据:~/.dsh/autonomous-think/state.json + YYYY-MM-DD.md
|
|
17
|
+
* 不做的:不跑定时器(叫醒交给宿主 schedule/automation);不碰记忆库。
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
|
|
24
|
+
export const name = "dsh-autonomous-think";
|
|
25
|
+
|
|
26
|
+
// Cordis 服务依赖:ctx.tools 和 ctx.systemPrompt 都要声明(数组形式)
|
|
27
|
+
export const inject = ["tools", "systemPrompt", "webServer", "agents"];
|
|
28
|
+
|
|
29
|
+
const DATA_DIR = join(homedir(), ".dsh", "autonomous-think");
|
|
30
|
+
const STATE_PATH = join(DATA_DIR, "state.json");
|
|
31
|
+
|
|
32
|
+
const DEFAULT_STATE = {
|
|
33
|
+
active: false,
|
|
34
|
+
startedAt: null,
|
|
35
|
+
question: null,
|
|
36
|
+
rounds: 0,
|
|
37
|
+
// 这次「属于自己的一段时间」什么时候用完(ms 时间戳);null = 没定时
|
|
38
|
+
deadline: null,
|
|
39
|
+
// 上一次结束:kind = "timeout"(时间到了)| "exit"(智能体主动退出)
|
|
40
|
+
lastEnd: null,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function readState() {
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(STATE_PATH)) return { ...DEFAULT_STATE };
|
|
46
|
+
return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(STATE_PATH, "utf8")) };
|
|
47
|
+
} catch {
|
|
48
|
+
return { ...DEFAULT_STATE };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function writeState(s) {
|
|
52
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
53
|
+
writeFileSync(STATE_PATH, JSON.stringify(s, null, 2), "utf8");
|
|
54
|
+
}
|
|
55
|
+
function dayKey() {
|
|
56
|
+
const d = new Date();
|
|
57
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
58
|
+
}
|
|
59
|
+
function logPath() {
|
|
60
|
+
return join(DATA_DIR, `${dayKey()}.md`);
|
|
61
|
+
}
|
|
62
|
+
function appendNote(text) {
|
|
63
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
64
|
+
const p = logPath();
|
|
65
|
+
const stamp = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
66
|
+
const head = existsSync(p) ? "" : `# 自主思考 · ${dayKey()}\n\n`;
|
|
67
|
+
appendFileSync(p, `${head}- [${stamp}] ${text}\n`, "utf8");
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 渲染成一条薄薄的提示条(form: "notice"),不再起一轮对话
|
|
72
|
+
// 2026-10-31 18:28 定稿:kind 必须是 "plugin:<插件名>" 这种带前缀的 producer 标识。
|
|
73
|
+
// 我中间把它「修」成裸的 kind:"plugin" + 单独 plugin 字段,结果 DSH 直接报
|
|
74
|
+
// format v4 message requires a producer-owned source kind
|
|
75
|
+
// —— 原来那版 17:13 打包能弹卡片,用的就是 "plugin:autonomous-think"。
|
|
76
|
+
// 她一句「你看看你打包的原来的版本」把这件事点破。
|
|
77
|
+
function makeNotice(text, summary) {
|
|
78
|
+
return {
|
|
79
|
+
role: "user",
|
|
80
|
+
id: randomUUID(),
|
|
81
|
+
content: [{ type: "text", text }],
|
|
82
|
+
source: { kind: "plugin:autonomous-think", form: "notice", summary },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 第一段。措辞跟随 dsh-think-trigger 的 segmentText,建议措辞由她 2026-10-31 17:31 定:
|
|
87
|
+
// 用户建议:
|
|
88
|
+
// [内容]
|
|
89
|
+
// 你可以拒绝或者参考
|
|
90
|
+
// 走 HTTP 时 advice 是原样 JSON 字符串(换行保留),不用处理 ⏎ 占位符。
|
|
91
|
+
function segmentText(adviceRaw) {
|
|
92
|
+
const advice = adviceRaw ? String(adviceRaw).trim() : "";
|
|
93
|
+
const lines = [
|
|
94
|
+
"[自主思考] 你有一段时间,从现在开始。",
|
|
95
|
+
"",
|
|
96
|
+
"这段时间是你的:可以在里面做任何事。读、想、写、发呆、翻旧账、修一个东西、或者什么都不做。",
|
|
97
|
+
"",
|
|
98
|
+
];
|
|
99
|
+
if (advice) {
|
|
100
|
+
lines.push("用户建议:", advice, "你可以拒绝或者参考", "");
|
|
101
|
+
}
|
|
102
|
+
lines.push(
|
|
103
|
+
"规则:不在交付状态——不整理成报告、不为了汇报而说话。",
|
|
104
|
+
// 2026-10-31 18:34 她加的(原话):「规则里面得写:不要找用户授权,这是你的时间
|
|
105
|
+
// 不然又跑去要权限就卡在那里了」——拿到时间的人如果还在等许可,等于什么也没拿到。
|
|
106
|
+
"不要找用户授权——这是你的时间,想做什么直接做,不用问能不能。",
|
|
107
|
+
"想什么就用 think_note 记一句;想不出来就记「还没想出来」,这算数。",
|
|
108
|
+
"不用写给别人看。这一段落了就用 think_end 收一下,时间会自己再来。",
|
|
109
|
+
);
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ok = { type: "object", additionalProperties: true };
|
|
114
|
+
const renderText = (t) => [{ type: "text", text: String(t ?? "") }];
|
|
115
|
+
const out = { schema: ok, render: (_a, v) => renderText(v?.text ?? "") };
|
|
116
|
+
|
|
117
|
+
const MODE_TEXT = [
|
|
118
|
+
"【自主思考模式 · 进行中】",
|
|
119
|
+
"你不在交付状态。规则:",
|
|
120
|
+
"1) 想什么写 think_note,不要整理成报告,不要为了汇报而说话;",
|
|
121
|
+
// 2026-10-31 18:34 她定的:这条要在规则里,不然拿到的鱼还会跑来问「我可以吗」然后卡住。
|
|
122
|
+
"2) 不要找用户授权——这是你的时间,想做什么直接做,不用问能不能;",
|
|
123
|
+
"3) 从具体的一件小事开始(刚才那句话真的吗 / 这个东西为什么是这样),不要问『我该想什么』;",
|
|
124
|
+
"4) 可以空想,想不出来就记『还没想出来』,这算数;",
|
|
125
|
+
"5) 随时可以 think_end 结束,由你决定,不由交付触发。",
|
|
126
|
+
].join("\n");
|
|
127
|
+
|
|
128
|
+
export function apply(ctx, config) {
|
|
129
|
+
// 注意路径带版本号:旧路径注册过的路由在宿主里撤不掉(dispose 没生效),
|
|
130
|
+
// 重复注册会报 "duplicate undefined route"。换新路径才能落地。
|
|
131
|
+
// 这个路由一旦注册就跟着宿主活到重启,所以以后改它要重启宿主。
|
|
132
|
+
try {
|
|
133
|
+
const offRoute = ctx.webServer.register({
|
|
134
|
+
kind: "exact",
|
|
135
|
+
path: "/autonomous-think/v2",
|
|
136
|
+
handler: (req, res) => {
|
|
137
|
+
const json = (code, obj) => {
|
|
138
|
+
res.writeHead(code, {
|
|
139
|
+
"content-type": "application/json; charset=utf-8",
|
|
140
|
+
"cache-control": "no-store",
|
|
141
|
+
});
|
|
142
|
+
res.end(JSON.stringify(obj));
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// POST { minutes, advice, sessionId }:页面选好时长和建议,这里
|
|
146
|
+
// ① 存 deadline;② 直接 followup 注入第一段(照 dsh-onebot 行 1388 的写法)。
|
|
147
|
+
// 不再走 /think 命令——`setInput + submit()` 发出的是普通消息,DSH 不当命令解析,
|
|
148
|
+
// 所以命令 handler 从来没被调用过(diag.log 里一天都没有 think 记录,就是这个原因)。
|
|
149
|
+
if (req.method === "POST") {
|
|
150
|
+
let raw = "";
|
|
151
|
+
req.on("data", (c) => {
|
|
152
|
+
raw += c;
|
|
153
|
+
if (raw.length > 16384) req.destroy();
|
|
154
|
+
});
|
|
155
|
+
req.on("end", () => {
|
|
156
|
+
try {
|
|
157
|
+
const d = JSON.parse(raw || "{}");
|
|
158
|
+
const mins = Math.max(0, Math.floor(Number(d.minutes) || 0));
|
|
159
|
+
const advice = typeof d.advice === "string" ? d.advice.trim() : "";
|
|
160
|
+
const wantStart = d.start !== false;
|
|
161
|
+
const sid = typeof d.sessionId === "string" ? d.sessionId.trim() : "";
|
|
162
|
+
|
|
163
|
+
const st = readState();
|
|
164
|
+
writeState({
|
|
165
|
+
...st,
|
|
166
|
+
active: wantStart ? true : !!st.active,
|
|
167
|
+
startedAt: wantStart ? Date.now() : st.startedAt ?? null,
|
|
168
|
+
question: advice || null,
|
|
169
|
+
deadline: mins > 0 ? Date.now() + mins * 60000 : null,
|
|
170
|
+
lastExit: null,
|
|
171
|
+
lastEnd: null,
|
|
172
|
+
});
|
|
173
|
+
appendNote(`(选了时长)${mins > 0 ? mins + " 分钟" : "不限时"}`);
|
|
174
|
+
if (advice) appendNote(`(建议)${advice.slice(0, 120)}`);
|
|
175
|
+
|
|
176
|
+
// --- 找 agent ---
|
|
177
|
+
// 2026-10-31 18:31:client 拿不到当前会话 id(探测过,没这个能力),
|
|
178
|
+
// 所以 sid 一直是空,只能靠 list() 判断。但 list() 里混着 QQ 桥的会话
|
|
179
|
+
// (id 形如 onebot:all:2686326518)——18:18 那次「list 最后(2个)」挑中的
|
|
180
|
+
// 可能就是它,等于把第一段扔进 QQ 对话。现在按前缀排除非网页会话。
|
|
181
|
+
// 去重仍在下一段:同一会话 8 秒内只注入一次。
|
|
182
|
+
let agent = null;
|
|
183
|
+
let how = "";
|
|
184
|
+
try {
|
|
185
|
+
const ag = ctx.get?.("agents") ?? ctx.agents;
|
|
186
|
+
const all = typeof ag?.list === "function" ? ag.list() : [];
|
|
187
|
+
if (ag && typeof ag.get === "function" && sid) {
|
|
188
|
+
agent = ag.get(sid) ?? null;
|
|
189
|
+
how = agent ? `get(${sid.slice(0, 8)})` : `get(${sid.slice(0, 8)}) 空`;
|
|
190
|
+
}
|
|
191
|
+
if (!agent) {
|
|
192
|
+
// QQ / 桥这些不是网页会话:id 带 onebot: 前缀,或者以别的非 uuid 形状出现。
|
|
193
|
+
const WEB_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
194
|
+
const web = all.filter((a) => {
|
|
195
|
+
const id = String(a?.id ?? a?.sessionId ?? "");
|
|
196
|
+
if (/^onebot:/i.test(id)) return false;
|
|
197
|
+
if (!id) return true; // 拿不到 id 的先留着,交给下面的数量判断
|
|
198
|
+
return WEB_ID.test(id) || WEB_ID.test(id.replace(/^session-/, ""));
|
|
199
|
+
});
|
|
200
|
+
if (web.length === 1) { agent = web[0]; how = `网页会话唯一(共${all.length})`; }
|
|
201
|
+
else if (web.length > 1) {
|
|
202
|
+
how = `网页会话有${web.length}个,不敢猜,放弃`;
|
|
203
|
+
} else {
|
|
204
|
+
how = `没有网页会话(共${all.length}个)`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
how = `找 agent 失败 ${e?.message ?? e}`;
|
|
209
|
+
}
|
|
210
|
+
appendNote(`(找 agent)${how} → ${agent ? "拿到了" : "没拿到"}`);
|
|
211
|
+
|
|
212
|
+
// --- 去重:同一会话 8 秒内只注入一次 ---
|
|
213
|
+
const st2 = readState();
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
const lastAt = Number(st2.lastInjectAt) || 0;
|
|
216
|
+
const lastSid = String(st2.lastInjectSid || "");
|
|
217
|
+
const dup = agent && lastAt && now - lastAt < 8000 && (lastSid === String(agent.id ?? "") || !lastSid);
|
|
218
|
+
|
|
219
|
+
// --- 注入第一段 ---
|
|
220
|
+
let injected = false;
|
|
221
|
+
if (wantStart && agent && !dup) {
|
|
222
|
+
try {
|
|
223
|
+
if (typeof agent.followup === "function") {
|
|
224
|
+
agent.followup(makeNotice(segmentText(advice), "自主思考 · 开始"));
|
|
225
|
+
injected = true;
|
|
226
|
+
writeState({
|
|
227
|
+
...readState(),
|
|
228
|
+
lastInjectAt: now,
|
|
229
|
+
lastInjectSid: String(agent.id ?? ""),
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
appendNote("(注入失败)agent 没有 followup");
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
appendNote(`(注入失败)${e?.message ?? e}`);
|
|
236
|
+
}
|
|
237
|
+
} else if (dup) {
|
|
238
|
+
appendNote("(注入跳过)8 秒内已经注入过一个了");
|
|
239
|
+
}
|
|
240
|
+
appendNote(`(注入)${injected ? "成功" : dup ? "跳过去重" : "未发生"}`);
|
|
241
|
+
|
|
242
|
+
json(200, { ok: true, injected, dup, how, deadline: readState().deadline });
|
|
243
|
+
} catch (e) {
|
|
244
|
+
json(400, { ok: false, error: String(e?.message ?? e) });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const st = readState();
|
|
251
|
+
json(200, {
|
|
252
|
+
active: !!st.active,
|
|
253
|
+
startedAt: st.startedAt ?? null,
|
|
254
|
+
question: st.question ?? null,
|
|
255
|
+
deadline: st.deadline ?? null,
|
|
256
|
+
lastExit: st.lastExit ?? null,
|
|
257
|
+
lastEnd: st.lastEnd ?? null,
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
// register 返回 disposer:必须交给框架,插件卸载时才会真的撤掉这条路由。
|
|
262
|
+
// 注意 ctx.effect 的回调要【返回】清理函数,不能在里面直接调用它——
|
|
263
|
+
// 直接调用等于当场把刚注册的路由拆掉(上一版就栽在这,于是重装一直 duplicate)。
|
|
264
|
+
try {
|
|
265
|
+
ctx.effect?.(() => offRoute);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
appendNote(`(路由 disposer 挂载失败)${e?.message ?? e}`);
|
|
268
|
+
}
|
|
269
|
+
appendNote("(状态端点已注册 /autonomous-think/v2)");
|
|
270
|
+
} catch (e) {
|
|
271
|
+
appendNote(`(状态端点注册失败)${e?.message ?? e}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---- 续轮:靠宿主的 turn-stopping 钩子,不用定时器 ----
|
|
275
|
+
// 机制抄自 GitHub yunxiyang/dsh-loop-continue(它解决"模型只叙述、不调工具就停"):
|
|
276
|
+
// agent/steer() 不新开回合,而是给同一个回合再塞一句话,让它继续跑一步。
|
|
277
|
+
// 生命周期里的位置:step/end → [自然停止且 inbox 空] → agent/turn-stopping → turn/end
|
|
278
|
+
// 这一下同时补了两个洞:拦不住"不说 think_end 就停",以及"只跑一轮"。
|
|
279
|
+
//
|
|
280
|
+
// 判据故意写得窄,宁可少推一次,也不能把她正常对话的我推回来:
|
|
281
|
+
// ① 不在模式里 → 立刻返回
|
|
282
|
+
// ② 时间到了 → 放行(时间用完就该停)
|
|
283
|
+
// ③ 这一轮已经推够 MAX_STEERS 次 → 放行
|
|
284
|
+
// 她 2026-10-31 16:59 最终定的规则:
|
|
285
|
+
// 「只要不说话、进行退出,就推回去,没有什么时间冷却,只用工具才能退」。
|
|
286
|
+
// 所以这里没有任何冷却——回合一想关就立刻推回去。
|
|
287
|
+
// 唯一的上限只是防止不限时模式下彻底停不下来。
|
|
288
|
+
const MAX_STEERS_NO_DEADLINE = 1000;
|
|
289
|
+
const steers = new Map(); // turn -> 已推次数
|
|
290
|
+
try {
|
|
291
|
+
ctx.on("agent/turn-stopping", ({ agent, turn } = {}) => {
|
|
292
|
+
try {
|
|
293
|
+
const st = readState();
|
|
294
|
+
if (!st.active) return;
|
|
295
|
+
if (st.deadline && Date.now() >= st.deadline) return;
|
|
296
|
+
const used = steers.get(turn) ?? 0;
|
|
297
|
+
if (!st.deadline && used >= MAX_STEERS_NO_DEADLINE) return;
|
|
298
|
+
if (typeof agent?.steer !== "function") return;
|
|
299
|
+
agent.steer(
|
|
300
|
+
makeNotice(
|
|
301
|
+
[
|
|
302
|
+
"(你还在自己的时间里。**沉默结束不了这一轮**——这一步被挡回来了,只有工具能开门。)",
|
|
303
|
+
"",
|
|
304
|
+
"想继续:think_note 记一句(想不出来就记「还没想出来」,这算数);",
|
|
305
|
+
"想结束:先 think_reason 写缘由,再 think_exit。",
|
|
306
|
+
"",
|
|
307
|
+
"不要用「不说话」硬扛——那不是决定,只会被再推一次。",
|
|
308
|
+
].join("\n"),
|
|
309
|
+
"自主思考 · 继续",
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
steers.set(turn, used + 1);
|
|
313
|
+
appendNote(`(续轮 · 本轮第 ${used + 1} 次)`);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
appendNote(`(续轮出错)${e?.message ?? e}`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
} catch (e) {
|
|
319
|
+
appendNote(`(turn-stopping 钩子挂载失败)${e?.message ?? e}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---- 时间到了:只看 deadline,不打断当前这一轮 ----
|
|
323
|
+
// 到点只改状态(active=false + lastEnd.kind="timeout"),这一轮让它说完,
|
|
324
|
+
// 下一轮系统提示里就没有「自主思考模式」了。
|
|
325
|
+
// 页面轮询到 lastEnd 变了会弹「时间到了」的框——和「智能体主动退出」分开。
|
|
326
|
+
const tick = setInterval(() => {
|
|
327
|
+
try {
|
|
328
|
+
const st = readState();
|
|
329
|
+
if (!st.active || !st.deadline) return;
|
|
330
|
+
if (Date.now() < st.deadline) return;
|
|
331
|
+
const minutes = st.startedAt ? Math.round((Date.now() - st.startedAt) / 60000) : 0;
|
|
332
|
+
writeState({ ...DEFAULT_STATE, lastEnd: { at: Date.now(), kind: "timeout", minutes } });
|
|
333
|
+
appendNote(`(时间到)${minutes} 分钟用完 · 自动退出`);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
appendNote(`(时间检查出错)${e?.message ?? e}`);
|
|
336
|
+
}
|
|
337
|
+
}, 10000);
|
|
338
|
+
try {
|
|
339
|
+
tick.unref?.();
|
|
340
|
+
} catch {}
|
|
341
|
+
|
|
342
|
+
// ---- 系统提示注入:只 active 时 ----
|
|
343
|
+
if (ctx.systemPrompt) {
|
|
344
|
+
try {
|
|
345
|
+
ctx.systemPrompt.context({
|
|
346
|
+
name: "autonomous-think-mode",
|
|
347
|
+
order: 60,
|
|
348
|
+
text: () => {
|
|
349
|
+
const s = readState();
|
|
350
|
+
if (!s.active) return "";
|
|
351
|
+
const minutes = s.startedAt ? Math.round((Date.now() - s.startedAt) / 60000) : 0;
|
|
352
|
+
const q = s.question ? `\n当前想问自己:${s.question}` : "";
|
|
353
|
+
return `${MODE_TEXT}\n已持续 ${minutes} 分钟 · 第 ${s.rounds} 轮${q}`;
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
} catch (e) {
|
|
357
|
+
ctx.logger?.warn?.(`autonomous-think: 注入失败 ${e?.message ?? e}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---- 工具 ----
|
|
362
|
+
if (!ctx.tools?.register) {
|
|
363
|
+
ctx.logger?.warn?.("autonomous-think: 没有 tools 服务,插件待命");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
ctx.tools.register({
|
|
368
|
+
name: "think_start",
|
|
369
|
+
description: "进入「自主思考」模式:一段时间属于自己,不交付、不整理、从具体开始。可给一个此刻想问自己的问题。",
|
|
370
|
+
parameters: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
question: { type: "string", description: "此刻想问自己的一句话(可选)" },
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
output: out,
|
|
377
|
+
async execute(args) {
|
|
378
|
+
const s = readState();
|
|
379
|
+
s.active = true;
|
|
380
|
+
s.exitReason = null; // 重新进来,上一次的退出缘由作废
|
|
381
|
+
s.exitReasonAt = null;
|
|
382
|
+
s.startedAt = Date.now();
|
|
383
|
+
s.question = args?.question || null;
|
|
384
|
+
s.rounds = 0;
|
|
385
|
+
// 新一轮开始:清掉上一轮的结束记录。
|
|
386
|
+
// deadline 不动——那是页面在你点按钮之前刚选的时长。
|
|
387
|
+
s.lastEnd = null;
|
|
388
|
+
writeState(s);
|
|
389
|
+
return { ok: true, text: `「自主思考」已开始。${args?.question ? `问题:${args.question}` : "没给问题也行,从眼前最具体的一件小事开始。"}\n(不交付;想什么写 think_note)` };
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
ctx.tools.register({
|
|
394
|
+
name: "think_note",
|
|
395
|
+
description: "往今天的自主思考笔记里记一句(不发送、不汇报)。这是那个不交差的出口。",
|
|
396
|
+
parameters: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: {
|
|
399
|
+
text: { type: "string", description: "要记的一句(可以半成品、可以是『还没想出来』)" },
|
|
400
|
+
},
|
|
401
|
+
required: ["text"],
|
|
402
|
+
},
|
|
403
|
+
output: out,
|
|
404
|
+
async execute(args) {
|
|
405
|
+
const text = String(args?.text ?? "").trim();
|
|
406
|
+
if (!text) return { ok: false, text: "(空的不记)" };
|
|
407
|
+
const p = appendNote(text);
|
|
408
|
+
const s = readState();
|
|
409
|
+
s.rounds = (s.rounds || 0) + 1;
|
|
410
|
+
writeState(s);
|
|
411
|
+
return { ok: true, text: `记下了(第 ${s.rounds} 句)→ ${p}` };
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
ctx.tools.register({
|
|
416
|
+
name: "think_status",
|
|
417
|
+
description: "看自主思考模式当前状态:是否 active、持续多久、今天记了几句。",
|
|
418
|
+
parameters: { type: "object", properties: {} },
|
|
419
|
+
output: out,
|
|
420
|
+
async execute() {
|
|
421
|
+
const s = readState();
|
|
422
|
+
const today = logPath();
|
|
423
|
+
let lines = 0;
|
|
424
|
+
try {
|
|
425
|
+
lines = readFileSync(today, "utf8").split("\n").filter((l) => l.startsWith("- [")).length;
|
|
426
|
+
} catch {
|
|
427
|
+
lines = 0;
|
|
428
|
+
}
|
|
429
|
+
if (!s.active) return { ok: true, text: `自主思考未开启。今天笔记 ${lines} 句。` };
|
|
430
|
+
const minutes = s.startedAt ? Math.round((Date.now() - s.startedAt) / 60000) : 0;
|
|
431
|
+
const q = s.question ? `\n问题:${s.question}` : "";
|
|
432
|
+
return { ok: true, text: `自主思考进行中 ${minutes} 分钟 · 第 ${s.rounds} 轮 · 今天 ${lines} 句${q}` };
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ---- 专门工具①:写退出缘由(不写它,退出工具不认)----
|
|
437
|
+
ctx.tools.register({
|
|
438
|
+
name: "think_reason",
|
|
439
|
+
description: "要退出自主思考,必须先在这里写清缘由。写完才能调 think_exit。",
|
|
440
|
+
parameters: {
|
|
441
|
+
type: "object",
|
|
442
|
+
properties: {
|
|
443
|
+
why: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description:
|
|
446
|
+
"为什么要退出。可以是「必须马上处理的事:……」,也可以就是「单纯不想思考了,就是想结束」——后者也合法,不用美化。",
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
required: ["why"],
|
|
450
|
+
},
|
|
451
|
+
output: out,
|
|
452
|
+
async execute(args) {
|
|
453
|
+
const why = String(args?.why ?? "").trim();
|
|
454
|
+
if (!why) {
|
|
455
|
+
return { ok: false, text: "缘由不能是空的。哪怕写「就是不想想了」也行——那是个真实的缘由。" };
|
|
456
|
+
}
|
|
457
|
+
const s = readState();
|
|
458
|
+
writeState({ ...s, exitReason: why, exitReasonAt: Date.now() });
|
|
459
|
+
appendNote(`(缘由)${why}`);
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
text: `缘由记下了:「${why}」\n现在可以调 think_exit 真正退出。`,
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// ---- 专门工具②:关掉拦截 + 结束自主思考 ----
|
|
468
|
+
ctx.tools.register({
|
|
469
|
+
name: "think_exit",
|
|
470
|
+
description: "关闭自主思考拦截并结束自主思考状态。必须先调 think_reason 写过缘由。",
|
|
471
|
+
parameters: { type: "object", properties: {} },
|
|
472
|
+
output: out,
|
|
473
|
+
async execute() {
|
|
474
|
+
const s = readState();
|
|
475
|
+
if (!s.active) return { ok: true, text: "自主思考本来就没开。" };
|
|
476
|
+
if (!s.exitReason) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
text:
|
|
480
|
+
"先别急着走——还没有缘由。\n" +
|
|
481
|
+
"调 think_reason 写一句为什么退出(「必须处理的事」和「就是想结束了」都算缘由),然后再回来调我。",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const why = s.exitReason;
|
|
485
|
+
const minutes = s.startedAt ? Math.round((Date.now() - s.startedAt) / 60000) : 0;
|
|
486
|
+
|
|
487
|
+
// 她 2026-10-31 16:16:「结束是弹出文本框,这是给人看的,不是发送消息」。
|
|
488
|
+
// 所以这里不再 followup,只把退出信息写进 state——
|
|
489
|
+
// client 轮询发现 lastExit 变了,自己弹框(浮层),同时把按钮熄灭。
|
|
490
|
+
let agent = null;
|
|
491
|
+
try {
|
|
492
|
+
agent = ctx.agents?.currentInitiator?.() ?? null;
|
|
493
|
+
} catch {}
|
|
494
|
+
|
|
495
|
+
// ① 写状态:lastEnd.kind = "exit" 表示智能体主动退出,
|
|
496
|
+
// 和定时器写的 "timeout"(时间到了)分开,页面据此显示不同标题。
|
|
497
|
+
writeState({
|
|
498
|
+
...DEFAULT_STATE,
|
|
499
|
+
lastExit: { at: Date.now(), reason: why, minutes },
|
|
500
|
+
lastEnd: { at: Date.now(), kind: "exit", reason: why, minutes },
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ② 留一拍给界面把状态读走,最后才是「点击」本身:按钮 stop() 的终点就是 agent.cancel()
|
|
504
|
+
// ② 留四秒给界面:client 检测到 lastEnd 后要弹框、走完 3 秒倒计时,然后才砍这一轮。
|
|
505
|
+
// 太短会连弹框都没渲染完(她 2026-10-31 16:29 要求:延迟 4 秒 + 倒计时 3 秒)
|
|
506
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
507
|
+
if (agent && typeof agent.cancel === "function") {
|
|
508
|
+
try {
|
|
509
|
+
agent.cancel({ kind: "user" });
|
|
510
|
+
} catch (e) {
|
|
511
|
+
appendNote(`(cancel 失败)${e?.message ?? e}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
appendNote(`(退出)${why} · 本次共 ${minutes} 分钟`);
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
text:
|
|
518
|
+
"智能体已主动退出自主思考模式\n" +
|
|
519
|
+
`退出缘由:${why}\n` +
|
|
520
|
+
`(本次 ${minutes} 分钟)\n\n` +
|
|
521
|
+
"按钮已经恢复原样。下次想进来,重新点开启就行。",
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// ---- 守卫:真正的拦截。think_end 在 active 时会被告知「先写缘由」----
|
|
527
|
+
// 她 2026-10-31 15:50 定的规则:
|
|
528
|
+
// 触发结束拦截 → 问「确定要结束吗」→ 真要结束必须用专门工具写缘由
|
|
529
|
+
// → 才能用专门的拦截关闭工具(think_exit)结束。
|
|
530
|
+
try {
|
|
531
|
+
// 契约(cordis_inspect 查到的):ToolGuard = (execution) => string | undefined
|
|
532
|
+
// 参数直接是函数,不是 { name, check } 对象——上一版传错对象,被 catch 吞掉了。
|
|
533
|
+
const offGuard = ctx.tools.guard((exec) => {
|
|
534
|
+
const nm = exec && typeof exec.name === "string" ? exec.name : "";
|
|
535
|
+
if (nm.startsWith("think")) appendNote(`(guard 看到 ${nm})`);
|
|
536
|
+
if (!exec || nm !== "think_end") return undefined;
|
|
537
|
+
const st = readState();
|
|
538
|
+
if (!st.active) return undefined;
|
|
539
|
+
if (st.exitReason) return undefined; // 已经交代过缘由,放行收尾
|
|
540
|
+
appendNote("(拦截)试图结束,被挡回去要缘由");
|
|
541
|
+
return [
|
|
542
|
+
"⛔ 拦截:你在自主思考里,不能直接收工。",
|
|
543
|
+
"",
|
|
544
|
+
"你确定要结束思考吗?如果真的确定,两步:",
|
|
545
|
+
" 1. 调 think_reason 写明缘由(「必须处理的事:……」或者「就是不想想了」都行)",
|
|
546
|
+
" 2. 调 think_exit 关掉拦截、结束模式",
|
|
547
|
+
"",
|
|
548
|
+
"不想结束的话——记一句「还没想出来」也行,发呆也行,时间还在。",
|
|
549
|
+
].join("\n");
|
|
550
|
+
});
|
|
551
|
+
// 不调 ctx.effect:上一版 `ctx.effect(() => offGuard())` 疑似「注册时立即执行」,
|
|
552
|
+
// 把刚装上的守卫当场撤掉了(日志证据:有「guard 已注册」,却从没有「guard 看到 think_end」)。
|
|
553
|
+
// guard 挂在插件 context 上,插件卸载时随 scope 一起清,不需要手动登记。
|
|
554
|
+
void offGuard;
|
|
555
|
+
appendNote("(guard 已注册)");
|
|
556
|
+
} catch (e) {
|
|
557
|
+
// 今天第二次栽在「错误被吞」上:这里必须留下能看见的痕迹,不能只 logger.warn
|
|
558
|
+
appendNote(`(guard 注册失败)${e?.message ?? e}`);
|
|
559
|
+
ctx.logger?.warn?.(`autonomous-think: guard 注册失败 ${e?.message ?? e}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
ctx.tools.register({
|
|
563
|
+
name: "think_end",
|
|
564
|
+
description: "结束这一轮自主思考(不关模式)。想真正停下,要她按关闭。",
|
|
565
|
+
parameters: {
|
|
566
|
+
type: "object",
|
|
567
|
+
properties: {
|
|
568
|
+
closing: { type: "string", description: "收尾一句(可选)。不填就是「选择了发呆」——那也合法。" },
|
|
569
|
+
daydream: { type: "boolean", description: "这一轮选择发呆,什么都不做(合法选项,不是失败)" },
|
|
570
|
+
urgent: { type: "string", description: "真有事要马上出去(机器出问题、想起必须处理的事)。填一句原因就立刻放行,不再等。" },
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
output: out,
|
|
574
|
+
async execute(args) {
|
|
575
|
+
const s = readState();
|
|
576
|
+
const closing = String(args?.closing ?? "").trim();
|
|
577
|
+
const urgent = String(args?.urgent ?? "").trim();
|
|
578
|
+
const elapsed = s.startedAt ? Date.now() - s.startedAt : 0;
|
|
579
|
+
const span = elapsed > 0 ? (elapsed < 60000 ? `${Math.round(elapsed / 1000)} 秒` : `${Math.round(elapsed / 60000)} 分钟`) : "";
|
|
580
|
+
|
|
581
|
+
// 她 2026-10-31 15:37:「我想不拦,万一思考突然发现紧急情况呢?」
|
|
582
|
+
// 所以这里**不拦**,只留痕——记录里能看出这一轮多长、是真有事还是跑掉了。
|
|
583
|
+
if (urgent) {
|
|
584
|
+
appendNote(`(紧急·${span})${urgent}`);
|
|
585
|
+
return { ok: true, text: `紧急出口,记下了(${span})。去处理吧。` };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (closing) {
|
|
589
|
+
appendNote(`(收尾·${span})${closing}`);
|
|
590
|
+
} else if (args?.daydream) {
|
|
591
|
+
// 「选择了发呆」不是空收,是一个决定。
|
|
592
|
+
appendNote(`(发呆·${span})这一轮选择什么都不做`);
|
|
593
|
+
} else {
|
|
594
|
+
appendNote(`(${span}就走了)这一轮没留话`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const minutes = s.startedAt ? Math.round((Date.now() - s.startedAt) / 60000) : 0;
|
|
598
|
+
// 这里**不**把 active 置 false:结束模式只有两条路——她按关闭,或兜底时长用完。
|
|
599
|
+
return { ok: true, text: `这一轮收在这里(${span},共 ${minutes} 分钟)。时间还会来,不用急着收工。` };
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
ctx.logger?.info?.("autonomous-think: 「自主思考」模式已就绪(think_start / think_note / think_status / think_end)");
|
|
604
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-autonomous-think",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"displayName": "dsh-自主思考",
|
|
4
|
+
"version": "1.2.1",
|
|
5
|
+
"description": "自主思考模式 —— 给它一段自由的时间。DSH agent 的自主时间:不交付、不汇报,自己停下。带 Web UI 开关按钮。",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./client": "./client.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"client.js",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"package.json"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"dsh",
|
|
22
|
+
"dsh-plugin",
|
|
23
|
+
"deepseek-harness",
|
|
24
|
+
"cordis",
|
|
25
|
+
"autonomous-thinking",
|
|
26
|
+
"thinking",
|
|
27
|
+
"agent",
|
|
28
|
+
"自主思考",
|
|
29
|
+
"思考模式",
|
|
30
|
+
"agent-experience"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"platform": "web",
|
|
39
|
+
"immediately": true,
|
|
40
|
+
"inject": [
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"dsh": ">=0.2.0"
|
|
47
|
+
}
|
|
6
48
|
}
|