cortico-world-humanize 0.1.0 → 0.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/CHANGELOG.md +19 -0
- package/README.md +7 -5
- package/describe.json +2 -2
- package/package.json +3 -3
- package/src/bridge.ts +19 -3
- package/src/config.ts +11 -1
- package/src/definition.ts +2 -2
- package/src/prefix-state.ts +20 -5
- package/src/provider-status.ts +47 -0
- package/src/world.ts +70 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- 新增开关「用「活人感 provider」」(在活人感的基本设置里,默认开)。Coopanion 的模型扩展没有停用开关,所以放在这里:关掉后 provider 原样转发、什么都不做,桌宠改回先调用 `humanize_feel`。不用卸载 provider 也能停用它。
|
|
6
|
+
- World 页新增「活人感 provider」状态灯和两个徽章,一眼看出 provider 有没有生效:
|
|
7
|
+
- **已生效**(绿):写明包着哪个端点;「provider 注入」记着这次启动后把心事塞进请求的次数。
|
|
8
|
+
- **待刷新**:装好了,还差把活人感关掉再打开一次。
|
|
9
|
+
- **待重启**:装了但还没加载,重启 Coopanion 才生效。
|
|
10
|
+
- **版本旧**:装的 provider 和这一版活人感对不上,要升级。
|
|
11
|
+
- **没包住**:当前端点的类型 provider 包不了。
|
|
12
|
+
- **已关闭**:装着,但上面的开关关了。
|
|
13
|
+
- **没装**:provider 是可选的,没装也能用。
|
|
14
|
+
- 没生效时,配置页顶部也写一句原因和该怎么做。
|
|
15
|
+
|
|
16
|
+
## 0.2.0
|
|
17
|
+
|
|
18
|
+
- 搭配「活人感 provider」0.2.0:装上 provider 重启就进入 provider 模式,不用再到「模型」页新建、切换端点。
|
|
19
|
+
- 还在用 0.1.0 时手动新建的「活人感」端点的,World 页会提示可以切回原来的端点;只有这种情况,卸载 provider 前才需要先切回去。
|
|
20
|
+
- 和 provider 之间的桥升到第 3 版:要和 `cortico-provider-humanize` 0.2.0 一起用。版本对不上时退回「先感受再开口」,不出错。
|
|
21
|
+
|
|
3
22
|
## 0.1.0
|
|
4
23
|
|
|
5
24
|
第一个公开版本。
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
## 安装
|
|
13
13
|
|
|
14
|
-
1. Coopanion
|
|
14
|
+
1. Coopanion 的「扩展」页搜「活人感」,安装 `cortico-world-humanize`。推荐把 `cortico-provider-humanize` 也装上(可选,装上就生效,不用设置)。
|
|
15
15
|
2. 重启 Coopanion。
|
|
16
16
|
3. World 页打开「活人感」。
|
|
17
17
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
| 方式 | 什么时候 | 效果 |
|
|
37
37
|
|---|---|---|
|
|
38
|
-
| **provider 模式** | 装了活人感 provider
|
|
38
|
+
| **provider 模式** | 装了活人感 provider(装上就自动包住你现在用的端点),且基本设置里「用「活人感 provider」」开着(默认开) | provider 在主模型请求发出前等心事,塞进请求再发。一轮就带着心事开口,最推荐 |
|
|
39
39
|
| 先感受再开口(默认) | 没用 provider | 桌宠先调用 `humanize_feel`,工具等副 API 想好再交回去。每次都赶得上,但主模型多一轮 |
|
|
40
40
|
| 抢时间 | 没用 provider,且关掉「先感受再开口」 | 心事作为事件推进去,桌宠已经开口就作废 |
|
|
41
41
|
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
- **配置**:基本设置、副 API、QQ 扩展、其他聊天来源
|
|
59
59
|
- **提示词**:预设库(情境、情绪、小动作、不想听到的说法)、环境提示词、口吻样本、最近 20 轮记录
|
|
60
60
|
- **数据**:清空最近记录和计数
|
|
61
|
-
- 导航栏:副 API
|
|
61
|
+
- 导航栏:副 API、系统前缀、活人感 provider 三个状态灯(悬停看详情),以及几个计数
|
|
62
|
+
- **provider 生没生效**:看「provider」徽章。「已生效」就是在用;「待刷新」「待重启」「版本旧」「没包住」会在配置页顶部写明怎么办。「provider 注入」是这次启动后心事塞进请求的次数,对桌宠说句话它就加一
|
|
62
63
|
|
|
63
64
|
## 系统前缀要刷新的时候
|
|
64
65
|
|
|
@@ -85,8 +86,9 @@ Coopanion 重启后沿用旧的系统前缀。换了模式或者活人感更新
|
|
|
85
86
|
|
|
86
87
|
## 停用和卸载
|
|
87
88
|
|
|
89
|
+
- **只停用 provider**:基本设置里关掉「用「活人感 provider」」,再把活人感关开一次。provider 原样转发,活人感改回「先感受再开口」。Coopanion 的模型扩展本身没有停用开关,所以放在这里。
|
|
88
90
|
- **暂时不用**:World 页关掉「活人感」。Coopanion 会立刻重建系统前缀,环境说明和 `humanize_feel` 工具一起拿掉。
|
|
89
|
-
-
|
|
91
|
+
- **卸载**:在「扩展」页卸掉两个包,然后重启。只有一种情况要先做一步:你用 0.1.0 时在「模型」页新建过「活人感」端点,而且当前端点还是它,那要**先把当前端点切回原来那个**,不然卸掉 provider 后桌宠会说不了话。
|
|
90
92
|
- **清干净**:想连数据一起去掉,删掉上面「数据和隐私」里列的两个文件夹和配置段即可。
|
|
91
93
|
|
|
92
94
|
## 开发
|
|
@@ -109,4 +111,4 @@ MIT
|
|
|
109
111
|
|
|
110
112
|
**Humanize** is a Cortico World for the Coopanion desktop pet. When someone talks to the pet, it reads the situation, the person's mood and the pet's own persona and memory files, and hands the pet a short inner thought before it answers. It also watches the pet's recent lines for stock AI sentence patterns ("not X but Y", parroting the other person's words back, either-or questions, aphorisms, and similar) and reminds the pet to say it another way when one shows up. It only targets sentence patterns: catchphrases, tone and length are left to the character.
|
|
111
113
|
|
|
112
|
-
Install it from Coopanion's Extensions page, restart, and turn it on in the World page. It works best together with [cortico-provider-humanize](https://github.com/abbabanana/cortico-provider-humanize), which puts the inner thought straight into the main model's request. Prompts, presets and detection are Chinese only for now. Licensed under MIT.
|
|
114
|
+
Install it from Coopanion's Extensions page, restart, and turn it on in the World page. It works best together with [cortico-provider-humanize](https://github.com/abbabanana/cortico-provider-humanize), which puts the inner thought straight into the main model's request; install it and restart, there is nothing to configure. Prompts, presets and detection are Chinese only for now. Licensed under MIT.
|
package/describe.json
CHANGED
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
{
|
|
30
30
|
"type": "humanize.notice",
|
|
31
31
|
"description": {
|
|
32
|
-
"zh": "
|
|
33
|
-
"en": "A reminder the pet passes on: the system prefix needs a refresh after a mode change or an update;
|
|
32
|
+
"zh": "请桌宠转告对方的提醒:换了模式或扩展更新后,系统前缀需要刷新;还在用旧版「活人感」端点时,卸载 provider 前要先切回原端点。每种变化一次。",
|
|
33
|
+
"en": "A reminder the pet passes on: the system prefix needs a refresh after a mode change or an update; on a legacy Humanize endpoint, switch back before uninstalling the provider. Once per change."
|
|
34
34
|
},
|
|
35
35
|
"trigger": "flush"
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-humanize",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cortico World for Coopanion: before the pet answers, reads the situation, mood and the pet's own memory and hands it a short inner thought, and reminds it when it slips into stock AI sentence patterns",
|
|
6
6
|
"author": "abbabanana",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"locales": {
|
|
24
24
|
"zh": {
|
|
25
25
|
"displayName": "活人感",
|
|
26
|
-
"description": "
|
|
26
|
+
"description": "让桌宠开口前先「想一想」,说话更像个人,少一点 AI 腔。单独装就能用,搭配「活人感 provider」更快。\n\n**它做什么**\n\n- 你对桌宠说话时,先读一下情境、你的心情和桌宠自己的记忆,递给它一句「此刻心里的想法」,再让它开口。\n- 桌宠要是用了「不是……而是……」、先复述你的话这类 AI 固定句式,下次就提醒它换个说法。口癖、语气、长短都不碰。\n\n**两个扩展怎么配合**\n\n- **活人感**(就是本扩展,必装):负责想心事、查句式。单独装就能用。\n- **活人感 provider**(可选,推荐):把心事直接放进发给模型的请求里。不装的话,桌宠每次要先调一次工具读心事再开口,多跑一轮,稍慢一点。\n- 只装 provider、不装本扩展,没有任何作用。\n\n**怎么用**\n\n1. 在扩展页装上本扩展,推荐把「活人感 provider」一起装上。\n2. 重启 Coopanion。\n3. 到 World 页打开「活人感」。\n\n主模型用的是 DeepSeek 的话,什么都不用填。用别家的话,在活人感的「配置 → 副 API」里填地址、模型和 Key。\n\n如果活人感已经开着、之后才装 provider,World 页会提示「系统前缀需要刷新」:把活人感关掉再打开一次就好,聊天记录会保留。\n\n**怎么看 provider 生没生效**:World 页「活人感」的「provider」徽章显示「已生效」就是在用;「provider 注入」是心事塞进请求的次数,对桌宠说句话就会加一。没生效时,配置页顶部会写明原因和该怎么做。\n\n**想停用 provider**:不用卸载,在活人感「配置 → 基本设置」里关掉「用「活人感 provider」」,再把活人感关掉打开一次。\n\n**要知道的**\n\n- 你每说一句话,会多调用一次副 API(默认 DeepSeek,约 1–2 秒,花费很少)。\n- 默认读桌宠、控制台、QQ 私聊和 @桌宠的消息。QQ 群里别人的话会经过副 API 影响心事,介意的话可以在配置里关掉。\n- 只支持中文,目前只在 Windows + DeepSeek 上测过。\n- 不想用了:World 页关掉即可。卸载:扩展页卸掉,重启。"
|
|
27
27
|
},
|
|
28
28
|
"en": {
|
|
29
29
|
"displayName": "Humanize",
|
|
30
|
-
"description": "
|
|
30
|
+
"description": "Lets the pet think for a moment before it speaks, so it sounds more like a person and less like an AI. Works on its own; faster with the Humanize provider.\n\n**What it does**\n\n- When you talk to the pet, it reads the situation, your mood and the pet's own memory, and hands the pet a short inner thought before it answers.\n- When the pet slips into stock AI sentence patterns (\"not X but Y\", parroting your words back), it is reminded to say it another way next time. Catchphrases, tone and length are left alone.\n\n**How the two extensions fit together**\n\n- **Humanize** (this one, required): writes the inner thought and checks sentence patterns. Works on its own.\n- **Humanize provider** (optional, recommended): puts the thought straight into the request sent to the model. Without it, the pet first calls a tool to read the thought, which costs one extra round.\n- The provider alone does nothing.\n\n**Getting started**\n\n1. Install this extension on the Extensions page, ideally together with the Humanize provider.\n2. Restart Coopanion.\n3. Turn on Humanize on the World page.\n\nIf your main model is DeepSeek, there is nothing to fill in. Otherwise enter the address, model and key under Config → Side API.\n\n**Good to know**\n\n- Each message you send adds one side-API call (DeepSeek by default, about 1–2 seconds, very cheap).\n- Prompts, presets and pattern checks are Chinese only. Tested on Windows with DeepSeek only.\n- To stop: turn it off on the World page. To uninstall: remove it on the Extensions page and restart."
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"tags": ["companion", "internal-state", "qq", "im", "high-cost", "injection-risk", "experimental"],
|
package/src/bridge.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* World 与「活人感」provider 之间的桥:两个包装在同一个进程里,靠 globalThis
|
|
3
|
-
* Symbol 碰头,谁也不 import 谁。
|
|
2
|
+
* World 与「活人感」provider 之间的桥:两个包装在同一个进程里,靠 globalThis 上约定好的
|
|
3
|
+
* Symbol 碰头,谁也不 import 谁。
|
|
4
|
+
* - BRIDGE_KEY:World 启动时挂上,停用时摘掉;provider 找不到就原样转发。
|
|
5
|
+
* - PROVIDER_KEY:provider 加载时挂上,说明它包住了哪些端点模块;World 据此判断走不走 provider 模式。
|
|
4
6
|
* 改这里的形状要同时改 cortico-provider-humanize/src/bridge.ts,并把 version 加一。
|
|
5
7
|
*/
|
|
6
8
|
export const BRIDGE_KEY = Symbol.for('cortico-humanize.bridge');
|
|
7
|
-
export const
|
|
9
|
+
export const PROVIDER_KEY = Symbol.for('cortico-humanize.provider');
|
|
10
|
+
export const BRIDGE_VERSION = 3;
|
|
8
11
|
|
|
9
12
|
/** 主模型这次请求里看到的东西,provider 原样转述(已截短)。 */
|
|
10
13
|
export interface MainContext {
|
|
@@ -28,6 +31,13 @@ export interface HumanizeBridge {
|
|
|
28
31
|
touch(): void;
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
/** provider 挂的牌子。 */
|
|
35
|
+
export interface ProviderMarker {
|
|
36
|
+
version: typeof BRIDGE_VERSION;
|
|
37
|
+
/** 这种端点模块(端点配置里的 kind)是不是被活人感包着。 */
|
|
38
|
+
wraps(kind: string): boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export function installBridge(bridge: HumanizeBridge): void {
|
|
32
42
|
(globalThis as Record<symbol, unknown>)[BRIDGE_KEY] = bridge;
|
|
33
43
|
}
|
|
@@ -36,3 +46,9 @@ export function removeBridge(bridge: HumanizeBridge): void {
|
|
|
36
46
|
const g = globalThis as Record<symbol, unknown>;
|
|
37
47
|
if (g[BRIDGE_KEY] === bridge) delete g[BRIDGE_KEY];
|
|
38
48
|
}
|
|
49
|
+
|
|
50
|
+
/** 装着的、版本对得上的 provider;没有给 null。 */
|
|
51
|
+
export function providerMarker(): ProviderMarker | null {
|
|
52
|
+
const m = (globalThis as Record<symbol, unknown>)[PROVIDER_KEY] as ProviderMarker | undefined;
|
|
53
|
+
return m && m.version === BRIDGE_VERSION && typeof m.wraps === 'function' ? m : null;
|
|
54
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface HumanizeConfigSection {
|
|
|
17
17
|
* 关掉则是抢时间模式:引导回来时桌宠还没开口才用得上。
|
|
18
18
|
*/
|
|
19
19
|
wait: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* 用「活人感」provider 把心事直接塞进主模型请求。provider 扩展本身没有开关(Coopanion 不支持
|
|
22
|
+
* 停用 provider 扩展),开关放在这里:关掉后 provider 原样转发,桌宠改回先调用 humanize_feel。
|
|
23
|
+
*/
|
|
24
|
+
useProvider: boolean;
|
|
20
25
|
/** 摸头、戳、换装这类小动作也带一句轻预设(不调副 API)。 */
|
|
21
26
|
light: boolean;
|
|
22
27
|
/** 小动作带轻预设的概率,0–1;动作频繁时别每次都带。 */
|
|
@@ -62,6 +67,7 @@ export const HUMANIZE_DEFAULTS: HumanizeConfigSection = {
|
|
|
62
67
|
enabled: false,
|
|
63
68
|
guide: true,
|
|
64
69
|
wait: true,
|
|
70
|
+
useProvider: true,
|
|
65
71
|
light: true,
|
|
66
72
|
lightChance: 0.5,
|
|
67
73
|
contextTurns: 6,
|
|
@@ -144,6 +150,10 @@ export function configGroups(keyStatus: string, notes: string[] = [], seen: Seen
|
|
|
144
150
|
description: '开着:桌宠收到你的话先等副 API 想好心情再回答,每次都用得上,但回应慢一秒左右。关掉:抢时间,桌宠开口太快时这一轮的引导就作废。改完在下一次会话开始时完全生效。',
|
|
145
151
|
'x-hot': true,
|
|
146
152
|
},
|
|
153
|
+
[`${K}.useProvider`]: {
|
|
154
|
+
type: 'boolean', title: '用「活人感 provider」', 'x-hot': true,
|
|
155
|
+
description: '装了「活人感 provider」才有用(没装时开着也没关系)。开着:心事直接跟着你的话发给模型,桌宠一轮就开口。关掉:provider 原样转发、什么都不做,桌宠改回先调用 humanize_feel。Coopanion 的模型扩展没有停用开关,所以开关放在这里。改完把活人感关掉再打开一次才完全生效。',
|
|
156
|
+
},
|
|
147
157
|
[`${K}.light`]: {
|
|
148
158
|
type: 'boolean', title: '小动作也带轻预设',
|
|
149
159
|
description: '被戳、被摸、被拎起、换装时,从预设库抽一句心情给它。', 'x-hot': true,
|
|
@@ -175,7 +185,7 @@ export function configGroups(keyStatus: string, notes: string[] = [], seen: Seen
|
|
|
175
185
|
},
|
|
176
186
|
[`${K}.notify`]: {
|
|
177
187
|
type: 'boolean', title: '前缀需要刷新时让桌宠提醒我', 'x-hot': true,
|
|
178
|
-
description: '
|
|
188
|
+
description: '换了模式(比如装上或卸掉 provider、开关「先感受再开口」),或者活人感更新了说明之后,桌宠会用气泡说一次。每种变化只提醒一次。',
|
|
179
189
|
},
|
|
180
190
|
[`${K}.autoRefresh`]: {
|
|
181
191
|
type: 'boolean', title: '前缀需要刷新时自动刷新', 'x-hot': true,
|
package/src/definition.ts
CHANGED
|
@@ -35,8 +35,8 @@ export const HUMANIZE: WorldDefinition<HumanizeConfigSection> = {
|
|
|
35
35
|
}, {
|
|
36
36
|
type: NOTICE_EVENT,
|
|
37
37
|
description: {
|
|
38
|
-
zh: '
|
|
39
|
-
en: 'A reminder the pet passes on: the system prefix needs a refresh after a mode change or an update;
|
|
38
|
+
zh: '请桌宠转告对方的提醒:换了模式或扩展更新后,系统前缀需要刷新;还在用旧版「活人感」端点时,卸载 provider 前要先切回原端点。每种变化一次。',
|
|
39
|
+
en: 'A reminder the pet passes on: the system prefix needs a refresh after a mode change or an update; on a legacy Humanize endpoint, switch back before uninstalling the provider. Once per change.',
|
|
40
40
|
},
|
|
41
41
|
trigger: 'flush',
|
|
42
42
|
}],
|
package/src/prefix-state.ts
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* 改了环境说明之后,前缀会旧一阵。这里记下最近一次拼前缀时的模式和环境说明的指纹(落盘,
|
|
5
5
|
* 重启也在),和此刻该有的比一比,不一样就提醒人去 World 页关开一次。
|
|
6
6
|
*
|
|
7
|
-
* provider 模式按当前端点的配置判断(部署 config.json 的 activeProvider → 那个端点的 kind
|
|
8
|
-
*
|
|
7
|
+
* provider 模式按当前端点的配置判断(部署 config.json 的 activeProvider → 那个端点的 kind,
|
|
8
|
+
* 再问 provider 挂的牌子包没包这种 kind),不看「最近有没有转发过请求」:重启后第一次拼前缀时
|
|
9
|
+
* provider 还没转发过,看时间会误判。
|
|
9
10
|
*/
|
|
10
11
|
import { createHash } from 'node:crypto';
|
|
11
12
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
12
13
|
import { dirname, join } from 'node:path';
|
|
14
|
+
import { providerMarker } from './bridge.ts';
|
|
13
15
|
|
|
14
16
|
export type Mode = 'provider' | 'tool' | 'push';
|
|
15
17
|
|
|
@@ -19,7 +21,8 @@ export const MODE_LABEL: Record<Mode, string> = {
|
|
|
19
21
|
push: '抢时间模式(心事作为事件推送)',
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
/** 0.1.0 时要手动新建的那种端点。 */
|
|
25
|
+
const LEGACY_KIND = 'humanize';
|
|
23
26
|
|
|
24
27
|
interface Saved {
|
|
25
28
|
prefix?: { mode: Mode; sig: string };
|
|
@@ -73,9 +76,21 @@ export class PrefixState {
|
|
|
73
76
|
return typeof name === 'string' ? name : '';
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
/**
|
|
79
|
+
/** 当前端点的模块(kind);读不到给空串。 */
|
|
80
|
+
activeKind(): string {
|
|
81
|
+
const kind = this.endpoint.read()?.kind;
|
|
82
|
+
return typeof kind === 'string' ? kind : '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 当前端点有没有被「活人感」provider 包着:装了 provider,且它包住了这种端点。 */
|
|
77
86
|
providerConfigured(): boolean {
|
|
78
|
-
|
|
87
|
+
const kind = this.activeKind();
|
|
88
|
+
return !!kind && !!providerMarker()?.wraps(kind);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 当前端点是不是 0.1.0 时手动新建的那种(卸 provider 前要先切回原端点)。 */
|
|
92
|
+
legacyEndpoint(): boolean {
|
|
93
|
+
return this.activeKind() === LEGACY_KIND;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
/** 拼前缀时记下当时的模式和指纹。 */
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「活人感 provider」此刻是什么状态,给 World 页的状态灯用。
|
|
3
|
+
*
|
|
4
|
+
* provider 加载后会在 globalThis 挂牌子(见 bridge.ts)。没挂牌子时分不清是「没装」「装了还没重启」
|
|
5
|
+
* 还是「装的是对不上的旧版」,所以再去扩展目录看一眼装没装、装的哪一版。
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const PACKAGE = 'cortico-provider-humanize';
|
|
12
|
+
/** 这一版 World 要求的 provider 最低版本(桥第 3 版)。 */
|
|
13
|
+
export const PROVIDER_MIN = '0.2.0';
|
|
14
|
+
const CACHE_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
const OWN_DIR = fileURLToPath(new URL('..', import.meta.url));
|
|
17
|
+
|
|
18
|
+
let cache: { at: number; botDir: string; version: string | null } | null = null;
|
|
19
|
+
|
|
20
|
+
/** 扩展目录里装着的 provider 版本;没装(或找不到)给 null。 */
|
|
21
|
+
export function installedProviderVersion(botDir: string): string | null {
|
|
22
|
+
if (cache && cache.botDir === botDir && Date.now() - cache.at < CACHE_MS) return cache.version;
|
|
23
|
+
const candidates = [
|
|
24
|
+
// Coopanion:<data>/home/<部署> 和 <data>/extensions/node_modules
|
|
25
|
+
join(dirname(dirname(botDir)), 'extensions', 'node_modules', PACKAGE, 'package.json'),
|
|
26
|
+
// 和本包装在同一个 node_modules 里
|
|
27
|
+
join(OWN_DIR, '..', PACKAGE, 'package.json'),
|
|
28
|
+
];
|
|
29
|
+
let version: string | null = null;
|
|
30
|
+
for (const file of candidates) {
|
|
31
|
+
try {
|
|
32
|
+
if (!existsSync(file)) continue;
|
|
33
|
+
const v = (JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }).version;
|
|
34
|
+
if (typeof v === 'string') { version = v; break; }
|
|
35
|
+
} catch { /* 读不了就当没找到 */ }
|
|
36
|
+
}
|
|
37
|
+
cache = { at: Date.now(), botDir, version };
|
|
38
|
+
return version;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** a < b(只比主、次、修订号)。 */
|
|
42
|
+
export function older(a: string, b: string): boolean {
|
|
43
|
+
const parse = (v: string) => v.split(/[.+-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
|
|
44
|
+
const [x, y] = [parse(a), parse(b)];
|
|
45
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
|
|
46
|
+
return false;
|
|
47
|
+
}
|
package/src/world.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - 有人对桌宠说话(桌宠、控制台、适配过的 QQ 扩展、勾上的其他来源):调副 API 读情境、写一句
|
|
5
5
|
* 内心引导,后面跟上「说话提醒」(桌宠最近真用过的套路句式)。交给桌宠有三条路:
|
|
6
|
-
* - provider
|
|
6
|
+
* - provider 模式(装了「活人感」provider,它包着当前端点):provider 在主模型请求发出前来认领,
|
|
7
7
|
* 顺带递来主模型的上下文;心事想好后由它塞进请求。主模型这一轮就带着心事开口。
|
|
8
8
|
* - 先感受再开口(`wait`,默认):环境提示词让模型先调用 `humanize_feel`,工具等副 API 的结果
|
|
9
9
|
* 回来,把内心交回给模型,模型读完再说话。
|
|
@@ -23,7 +23,8 @@ import type {
|
|
|
23
23
|
} from 'cortico/core/types.ts';
|
|
24
24
|
import { nowIso } from 'cortico/core/util.ts';
|
|
25
25
|
import { analyze, type Line, type Reading } from './analyzer.ts';
|
|
26
|
-
import { BRIDGE_VERSION, installBridge, removeBridge, type HumanizeBridge, type MainContext } from './bridge.ts';
|
|
26
|
+
import { BRIDGE_VERSION, installBridge, providerMarker, removeBridge, type HumanizeBridge, type MainContext } from './bridge.ts';
|
|
27
|
+
import { PROVIDER_MIN, installedProviderVersion, older } from './provider-status.ts';
|
|
27
28
|
import { HUMANIZE_ID, KEY_SECRET, configGroups, type HumanizeConfigSection } from './config.ts';
|
|
28
29
|
import { FALLBACK_EMOTION, PresetStore, emotionsOf, type Presets, type Scene } from './presets.ts';
|
|
29
30
|
import { PersonaStore } from './persona.ts';
|
|
@@ -179,7 +180,8 @@ export class HumanizeWorld implements World {
|
|
|
179
180
|
private readonly bridge: HumanizeBridge = {
|
|
180
181
|
version: BRIDGE_VERSION,
|
|
181
182
|
sync: () => this.tick(),
|
|
182
|
-
|
|
183
|
+
// 关了「用活人感 provider」:不交心事,provider 原样转发
|
|
184
|
+
claim: (cursors, main) => (this.useProvider() ? this.claim(cursors, main) : null),
|
|
183
185
|
touch: () => { this.providerAt = Date.now(); },
|
|
184
186
|
};
|
|
185
187
|
/** 最近一次想好的内心,工具来晚了也能交回去。 */
|
|
@@ -192,7 +194,9 @@ export class HumanizeWorld implements World {
|
|
|
192
194
|
private lastUserAt: number | null = null;
|
|
193
195
|
private readonly recentLines: string[] = [];
|
|
194
196
|
private readonly log: LogEntry[] = [];
|
|
195
|
-
private readonly stats = { api: 0, preset: 0, late: 0, light: 0, apiMs: 0, waited: 0 };
|
|
197
|
+
private readonly stats = { api: 0, preset: 0, late: 0, light: 0, apiMs: 0, waited: 0, injected: 0 };
|
|
198
|
+
/** provider 最近一次把心事塞进请求的时刻(时:分:秒)。 */
|
|
199
|
+
private lastInjectedAt = '';
|
|
196
200
|
private keyNote = '';
|
|
197
201
|
private readonly presets: PresetStore;
|
|
198
202
|
private readonly persona: PersonaStore;
|
|
@@ -227,10 +231,16 @@ export class HumanizeWorld implements World {
|
|
|
227
231
|
* 先看当前端点的配置(拼前缀时 provider 可能还没转发过),读不到再看它最近有没有报到。
|
|
228
232
|
*/
|
|
229
233
|
private providerLive(): boolean {
|
|
234
|
+
if (!this.useProvider()) return false;
|
|
230
235
|
return this.prefixState.providerConfigured()
|
|
231
236
|
|| (this.providerAt !== null && Date.now() - this.providerAt < PROVIDER_LIVE_MS);
|
|
232
237
|
}
|
|
233
238
|
|
|
239
|
+
/** 配置里的「用活人感 provider」;旧配置没有这一项时算开着。 */
|
|
240
|
+
private useProvider(): boolean {
|
|
241
|
+
return this.cfg.useProvider !== false;
|
|
242
|
+
}
|
|
243
|
+
|
|
234
244
|
private mode(): Mode {
|
|
235
245
|
return this.providerLive() ? 'provider' : this.cfg.wait ? 'tool' : 'push';
|
|
236
246
|
}
|
|
@@ -479,6 +489,8 @@ export class HumanizeWorld implements World {
|
|
|
479
489
|
return thinking.promise.then((feeling) => {
|
|
480
490
|
if (!feeling) return null;
|
|
481
491
|
this.stats.waited++;
|
|
492
|
+
this.stats.injected++;
|
|
493
|
+
this.lastInjectedAt = nowIso(this.opts.timezone).slice(11, 19);
|
|
482
494
|
this.count(feeling);
|
|
483
495
|
this.record({ trigger: feeling.via, outcome: feeling.outcome, text: feeling.body, ms: feeling.ms, error: feeling.error, how: `经 provider 注入,主模型晚发了 ${Date.now() - started}ms` });
|
|
484
496
|
return feeling.body;
|
|
@@ -757,8 +769,8 @@ export class HumanizeWorld implements World {
|
|
|
757
769
|
const what = stale.from !== stale.to
|
|
758
770
|
? `换成了${MODE_LABEL[stale.to]}`
|
|
759
771
|
: '更新了给桌宠的说明';
|
|
760
|
-
const uninstall = stale.to === 'provider'
|
|
761
|
-
? '
|
|
772
|
+
const uninstall = stale.to === 'provider' && this.prefixState.legacyEndpoint()
|
|
773
|
+
? '另外记一下:现在用的是旧版的「活人感」端点,以后要卸载「活人感 provider」的话,先在「模型」页把端点切回原来那个,不然我会说不了话。'
|
|
762
774
|
: '';
|
|
763
775
|
if (this.cfg.autoRefresh) {
|
|
764
776
|
if (this.phase !== 'idle' || !this.prefixState.firstTime(`refresh:${key}`)) return;
|
|
@@ -780,6 +792,48 @@ export class HumanizeWorld implements World {
|
|
|
780
792
|
}
|
|
781
793
|
}
|
|
782
794
|
|
|
795
|
+
/**
|
|
796
|
+
* 「活人感 provider」生没生效,给 World 页看:状态灯(悬停看详情)、徽章上的一两个字,
|
|
797
|
+
* 以及没生效时放在配置页顶部的一句说明。
|
|
798
|
+
*/
|
|
799
|
+
private providerStatus(): { lamp: WorldLamp; badge: string; note?: string } {
|
|
800
|
+
const label = '活人感 provider';
|
|
801
|
+
const endpoint = this.prefixState.activeEndpoint() || '(未选)';
|
|
802
|
+
const kind = this.prefixState.activeKind();
|
|
803
|
+
if (providerMarker()) {
|
|
804
|
+
if (!this.useProvider()) {
|
|
805
|
+
const stale = this.staleness();
|
|
806
|
+
const hint = stale && stale.from === 'provider'
|
|
807
|
+
? '已在配置里关掉,还差一步:把活人感关掉再打开一次'
|
|
808
|
+
: '已装好,但在配置里关掉了:provider 原样转发,桌宠先调用 humanize_feel 再开口。想用就打开「用「活人感 provider」」。';
|
|
809
|
+
return { lamp: { label, state: 'offline', hint }, badge: '已关闭' };
|
|
810
|
+
}
|
|
811
|
+
if (!this.prefixState.providerConfigured()) {
|
|
812
|
+
const why = `活人感 provider 已装好,但没包住当前端点「${endpoint}」${kind ? `(类型 ${kind})` : ''},这个端点不会带上活人感,桌宠照旧先调用 humanize_feel。`;
|
|
813
|
+
return { lamp: { label, state: 'error', hint: why }, badge: '没包住', note: why };
|
|
814
|
+
}
|
|
815
|
+
const stale = this.staleness();
|
|
816
|
+
if (stale && stale.to === 'provider' && stale.from !== 'provider') {
|
|
817
|
+
return { lamp: { label, state: 'loading', hint: '已装好,还差一步:把活人感关掉再打开一次' }, badge: '待刷新' };
|
|
818
|
+
}
|
|
819
|
+
const n = this.stats.injected;
|
|
820
|
+
const hint = n
|
|
821
|
+
? `已生效:包着端点「${endpoint}」。这次启动已注入 ${n} 次,最近一次 ${this.lastInjectedAt}。`
|
|
822
|
+
: `已生效:包着端点「${endpoint}」。对桌宠说句话,「provider 注入」就会加一。`;
|
|
823
|
+
return { lamp: { label, state: 'online', hint }, badge: '已生效' };
|
|
824
|
+
}
|
|
825
|
+
const version = installedProviderVersion(this.opts.botDir);
|
|
826
|
+
if (!version) {
|
|
827
|
+
return { lamp: { label, state: 'offline', hint: '没装(可选):装上「活人感 provider」并重启,桌宠每次能少跑一轮。' }, badge: '没装' };
|
|
828
|
+
}
|
|
829
|
+
if (older(version, PROVIDER_MIN)) {
|
|
830
|
+
const why = `装着的活人感 provider 是 ${version},太旧了,和这一版活人感对不上,现在不起作用(不会出错,只是桌宠每次多跑一轮)。到「扩展」页把它升到最新版,然后重启 Coopanion。`;
|
|
831
|
+
return { lamp: { label, state: 'error', hint: why }, badge: '版本旧', note: why };
|
|
832
|
+
}
|
|
833
|
+
const why = `活人感 provider ${version} 已安装,但还没加载:重启一次 Coopanion 才生效。`;
|
|
834
|
+
return { lamp: { label, state: 'error', hint: why }, badge: '待重启', note: why };
|
|
835
|
+
}
|
|
836
|
+
|
|
783
837
|
/** 请桌宠用一两句话转告对方。和 Coopanion 自己告诉桌宠设置变了是同一个路子。 */
|
|
784
838
|
private async notice(text: string): Promise<void> {
|
|
785
839
|
const host = this.host;
|
|
@@ -808,8 +862,10 @@ export class HumanizeWorld implements World {
|
|
|
808
862
|
if (this.cfg.voice && voice.outdated && voice.edited) {
|
|
809
863
|
notes.push('人设改过了,但口吻样本是按旧人设写的(你改过它,所以没有自动重写):想按新人设重写,勾一下下面的「重写口吻样本」。');
|
|
810
864
|
}
|
|
811
|
-
|
|
812
|
-
|
|
865
|
+
const provider = this.providerStatus();
|
|
866
|
+
if (provider.note) notes.push(provider.note);
|
|
867
|
+
if (this.prefixState.legacyEndpoint()) {
|
|
868
|
+
notes.push(`当前端点「${this.prefixState.activeEndpoint()}」是旧版的「活人感」端点。现在不用它了:装着活人感 provider 时,你原来的端点会自动带上活人感。建议在「模型」页切回原来的端点;以后要卸载 provider,也一定要先切回去,不然桌宠会说不了话。`);
|
|
813
869
|
}
|
|
814
870
|
return notes;
|
|
815
871
|
}
|
|
@@ -871,7 +927,8 @@ export class HumanizeWorld implements World {
|
|
|
871
927
|
stat: () => `${this.log.length} 条`,
|
|
872
928
|
clear: () => {
|
|
873
929
|
this.log.length = 0;
|
|
874
|
-
Object.assign(this.stats, { api: 0, preset: 0, late: 0, light: 0, apiMs: 0, waited: 0 });
|
|
930
|
+
Object.assign(this.stats, { api: 0, preset: 0, late: 0, light: 0, apiMs: 0, waited: 0, injected: 0 });
|
|
931
|
+
this.lastInjectedAt = '';
|
|
875
932
|
this.writeLog();
|
|
876
933
|
return '已清空';
|
|
877
934
|
},
|
|
@@ -881,11 +938,14 @@ export class HumanizeWorld implements World {
|
|
|
881
938
|
const prefixLamp: WorldLamp = stale
|
|
882
939
|
? { label: '系统前缀', state: this.cfg.autoRefresh ? 'loading' : 'error', hint: this.cfg.autoRefresh ? '等桌宠空闲后自动刷新' : '需要刷新:把活人感关掉再打开一次' }
|
|
883
940
|
: { label: '系统前缀', state: 'online', hint: `最新(${MODE_LABEL[this.mode()]})` };
|
|
941
|
+
const provider = this.providerStatus();
|
|
884
942
|
return {
|
|
885
943
|
label: '活人感',
|
|
886
|
-
lamps: [lamp, prefixLamp],
|
|
944
|
+
lamps: [lamp, prefixLamp, provider.lamp],
|
|
887
945
|
badges: [
|
|
888
946
|
{ label: '前缀', value: stale ? '需要刷新' : '最新', tone: stale ? 'off' : 'on' },
|
|
947
|
+
{ label: 'provider', value: provider.badge, tone: provider.lamp.state === 'online' ? 'on' : provider.lamp.state === 'offline' ? 'plain' : 'off' },
|
|
948
|
+
{ label: 'provider 注入', value: this.stats.injected, tone: this.stats.injected ? 'on' : 'plain' },
|
|
889
949
|
{ label: '副 API 引导', value: this.stats.api, tone: 'on' },
|
|
890
950
|
{ label: '预设兜底', value: this.stats.preset, tone: 'plain' },
|
|
891
951
|
{ label: '等内心再开口', value: this.stats.waited, tone: 'plain' },
|