cortico 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/README.md +3 -3
- package/package.json +1 -1
- package/src/boot.ts +29 -0
- package/src/bot.ts +40 -5
- package/src/core/README.md +12 -2
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +18 -6
- package/src/core/generation.ts +66 -9
- package/src/core/instance-lock.ts +6 -0
- package/src/core/loop.ts +62 -32
- package/src/core/secrets.ts +2 -4
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +23 -4
- package/src/core/util.ts +15 -0
- package/src/deploy.ts +5 -4
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +34 -6
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +30 -22
- package/src/protocol/open-responses/context-helpers.ts +3 -1
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +44 -12
- package/src/providers/base.ts +2 -0
- package/src/providers/console/config.ts +36 -0
- package/src/providers/console/hub.ts +307 -0
- package/src/providers/console/settings.ts +30 -26
- package/src/providers/console/strings.ts +14 -0
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +38 -0
- package/src/providers/llamacpp/console/models-panel.ts +87 -4
- package/src/providers/llamacpp/console/runtime-panel.ts +36 -73
- package/src/providers/llamacpp/console/server.ts +47 -4
- package/src/providers/llamacpp/huggingface.ts +93 -0
- package/src/providers/llamacpp/index.ts +5 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/options.ts +2 -0
- package/src/providers/llamacpp/strings.ts +42 -0
- package/src/providers/name.ts +13 -0
- package/src/providers/openai-responses-compat/config.ts +33 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +135 -0
- package/src/providers/openai-responses-compat/index.ts +40 -12
- package/src/providers/openai-responses-compat/native.ts +15 -5
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/pricebook.ts +5 -5
- package/src/providers/registry.ts +24 -10
- package/src/providers/strings.ts +2 -0
- package/src/providers/transport/errors.ts +9 -0
- package/src/providers/transport/response-http.ts +7 -3
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +15 -3
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +42 -35
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +2 -4
- package/src/web/client/console-pages/host.ts +22 -5
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +1 -1
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +27 -11
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +9 -3
- package/src/web/client/features/providers/detail.ts +309 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +179 -87
- package/src/web/client/features/providers/strings.ts +48 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +12 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +1 -1
- package/src/web/client/ui/fields.ts +3 -1
- package/src/web/client/ui/icons.ts +9 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +130 -18
- package/src/web/server.ts +229 -21
- package/src/web/shared/client-panel.ts +3 -0
- package/src/web/shared/console-protocol.ts +16 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +8 -0
- package/src/worlds/minecraft/client-launch.ts +11 -2
- package/src/worlds/minecraft/client.ts +4 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +56 -10
- package/src/worlds/terminal/world.ts +3 -4
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
实时游戏 World:以 mineflayer 玩家客户端连原版服务器,把"游戏状态 → 文字观察"
|
|
6
6
|
与"高层意图 → 游戏操作"接通。
|
|
7
7
|
|
|
8
|
+
**症状跟服务端版本有关就先读 [ADAPT.md](ADAPT.md)。** 那里按版本记着已知的服务端判据
|
|
9
|
+
差异、在日志里认出它的特征、以及客户端这边的改法。
|
|
10
|
+
|
|
11
|
+
新量到一条就补进 ADAPT.md,并在情况允许时向操作员申请提一个 PR。通用的适配问题连代码
|
|
12
|
+
一起提;改动牵涉上游(mineflayer、prismarine-*、服务端行为)时,代码不一定要提,那条经验
|
|
13
|
+
仍然值得提。
|
|
14
|
+
|
|
8
15
|
## 结构
|
|
9
16
|
|
|
10
17
|
```
|
|
@@ -523,6 +530,7 @@ surface 是尽力而为:先浮上水面换气(这步永远做得到),再
|
|
|
523
530
|
2. **合成前清空合成格。** 光标物品无法放回时返回错误;徒手 2×2 合成使用不会关闭的窗口 0,失败后由补丁取回格内材料。
|
|
524
531
|
3. **放置后回读目标格。** 每次等待 400ms,最多尝试三次;最终错误保留 Mineflayer 的文本契约,供寻路器处理。
|
|
525
532
|
4. **丢弃 `windowId=-2` 的 `set_slot`。** 其他窗口的 `set_slot` 和 `window_items` 继续传递;已收到当前窗口的 `stateId` 时,用该值替换其他窗口包中的 `stateId`。
|
|
533
|
+
5. **水平碰撞停在离方块面 `WALL_GAP` 处。** 服务端拒绝包围盒恰好贴着方块侧面的位置(静默传送回上一个好位置,不打 `moved wrongly`),prismarine-physics 原本停在贴面处;竖直方向贴面服务端照收,不改。改的是 `prismarine-physics` 的 AABB 原型,bot 物理与寻路器模拟同时生效。
|
|
526
534
|
|
|
527
535
|
合成与放置期间的包流记为 World 日志 `craft` 类别(`click-out`、`slot-in`、
|
|
528
536
|
`items-in`、`stateid-guard`、`result-slot`、`grid-clear`);窗口外的高频 `set_slot`
|
|
@@ -65,6 +65,8 @@ interface ClientLaunch {
|
|
|
65
65
|
nativeJars: string[];
|
|
66
66
|
/** 实际用到的版本 id 链,child → root */
|
|
67
67
|
versionChain: string[];
|
|
68
|
+
/** 版本 JSON 点名、但 libraries 目录里没有的条目,相对 libraries 目录。类路径里少了它们。 */
|
|
69
|
+
missingLibraries: string[];
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const OS_NAMES: Partial<Record<NodeJS.Platform, string>> = {
|
|
@@ -202,6 +204,7 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
202
204
|
const librariesDir = join(gameDir, 'libraries');
|
|
203
205
|
const classpath: string[] = [];
|
|
204
206
|
const nativeJars: string[] = [];
|
|
207
|
+
const missingLibraries: string[] = [];
|
|
205
208
|
const seenPaths = new Set<string>();
|
|
206
209
|
for (const version of chain) {
|
|
207
210
|
for (const lib of version.libraries ?? []) {
|
|
@@ -211,7 +214,10 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
211
214
|
const abs = join(librariesDir, rel);
|
|
212
215
|
if (seenPaths.has(abs)) continue;
|
|
213
216
|
seenPaths.add(abs);
|
|
214
|
-
if (!existsSync(abs))
|
|
217
|
+
if (!existsSync(abs)) {
|
|
218
|
+
missingLibraries.push(rel);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
215
221
|
if (/natives/i.test(rel)) nativeJars.push(abs);
|
|
216
222
|
classpath.push(abs);
|
|
217
223
|
}
|
|
@@ -222,7 +228,9 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
222
228
|
const separator = os === 'windows' ? ';' : ':';
|
|
223
229
|
const vars: Record<string, string> = {
|
|
224
230
|
auth_player_name: input.username,
|
|
225
|
-
version_name
|
|
231
|
+
// 加载器版本靠 `-DignoreList=...,${version_name}.jar` 把主 jar 挡在模块路径外,所以这个名字
|
|
232
|
+
// 必须是实际加载的那个 jar;继承链上的版本没有自己的 jar,用的是 jarId 那个。
|
|
233
|
+
version_name: jarId,
|
|
226
234
|
game_directory: gameDir,
|
|
227
235
|
assets_root: join(gameDir, 'assets'),
|
|
228
236
|
game_assets: join(gameDir, 'assets'),
|
|
@@ -268,5 +276,6 @@ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { er
|
|
|
268
276
|
mainClass,
|
|
269
277
|
nativeJars,
|
|
270
278
|
versionChain: chain.map((v, i) => v.id ?? (i === 0 ? versionId : '?')),
|
|
279
|
+
missingLibraries,
|
|
271
280
|
};
|
|
272
281
|
}
|
|
@@ -215,6 +215,10 @@ export class GameClient {
|
|
|
215
215
|
return this.state();
|
|
216
216
|
}
|
|
217
217
|
this.activeGameDir = gameDir;
|
|
218
|
+
if ('missingLibraries' in launch && launch.missingLibraries.length > 0) {
|
|
219
|
+
this.opts.log.warn(`${this.opts.label}版本 JSON 点名的 ${launch.missingLibraries.length} 个库`
|
|
220
|
+
+ `不在 libraries 目录,类路径里没有它们:${launch.missingLibraries.join(' ')}`);
|
|
221
|
+
}
|
|
218
222
|
if ('nativeJars' in launch) prepareNatives(launch.nativeJars, launch.nativesDir, this.opts.log);
|
|
219
223
|
if (!this.opts.commandOverride) {
|
|
220
224
|
if (this.opts.noPauseOnLostFocus()) {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* 一律本地换位,原版服务端不换。一次换位两边状态就此分家,之后所有合成都在错的格子上做。
|
|
9
9
|
*
|
|
10
10
|
* 光照段随包到来时必须按 nibble 原序落位,prismarine-chunk 把它当长整型数组读了。
|
|
11
|
+
*
|
|
12
|
+
* 水平碰撞停在离方块面 `WALL_GAP` 处:服务端拒绝包围盒恰好贴着方块侧面的位置(见 `installWallGap`)。
|
|
11
13
|
*/
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
12
15
|
import type { Bot } from 'mineflayer';
|
|
13
16
|
import type { Logger } from '../../core/types.ts';
|
|
14
17
|
import type { MinecraftLog } from './log.ts';
|
|
@@ -36,6 +39,11 @@ const DIG_CONFIRM_FACTOR = 3;
|
|
|
36
39
|
const DIG_GROUND_WAIT_MS = 600;
|
|
37
40
|
/** 服务端认账比本地定时器晚这么多就记一条:整片挖掘不刷屏,慢的那些留痕 */
|
|
38
41
|
const DIG_SLOW_ACK_MS = 200;
|
|
42
|
+
/**
|
|
43
|
+
* 水平碰撞停在离方块面这么远的地方。服务端只要缝隙大于零就收(实测 1e-7 也收),
|
|
44
|
+
* 取 1e-5 让坐标到三千万格时的浮点误差也吃不掉它。
|
|
45
|
+
*/
|
|
46
|
+
export const WALL_GAP = 1e-5;
|
|
39
47
|
|
|
40
48
|
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
41
49
|
|
|
@@ -136,7 +144,53 @@ export function installMineflayerFixes(
|
|
|
136
144
|
installConfirmedCraft(bot, diag, trace, (n) => { tracing += n; }, showTempo);
|
|
137
145
|
installLightRelay(bot, log, diag);
|
|
138
146
|
installDismountFix(bot, diag);
|
|
139
|
-
|
|
147
|
+
installWallGap();
|
|
148
|
+
log.info(`mineflayer 修补已装上:合成取服务端产物、放置短超时重发、附魔按组件格式计入挖掘、挖掘等服务端改掉那一格、stateId 认当前窗口、光照段重新落位、水平碰撞留缝${diag ? '' : '(没给 World 日志,包流不留痕)'}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 原型上的标记:同一进程里多个连接、多份模块实例只改一次 */
|
|
152
|
+
const WALL_GAP_MARK = Symbol.for('cortico.minecraft.wallGap');
|
|
153
|
+
|
|
154
|
+
interface PhysicsBox {
|
|
155
|
+
minX: number; minY: number; minZ: number; maxX: number; maxY: number; maxZ: number;
|
|
156
|
+
computeOffsetX(other: PhysicsBox, offset: number): number;
|
|
157
|
+
computeOffsetZ(other: PhysicsBox, offset: number): number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 1.21.11 的服务端把「包围盒恰好贴着方块侧面」的位置当成撞进方块:不打 moved wrongly,
|
|
162
|
+
* 静默把人传送回上一个好位置(1.21.10 及更早照收)。prismarine-physics 的碰撞解算恰好停在
|
|
163
|
+
* 贴面处,于是贴墙、上台阶、贴墙起跳每一拍都被打回,人钉在原地。竖直方向贴面照收,这里只改 X/Z:
|
|
164
|
+
* 朝方块走的那一步停在离面 `WALL_GAP` 处;已经贴着(服务端传送来的位置就是贴面的)
|
|
165
|
+
* 再往里走时退开同样的距离。bot 的物理与寻路器的模拟共用这一个类,一处改两处生效。
|
|
166
|
+
*/
|
|
167
|
+
export function installWallGap(): void {
|
|
168
|
+
const mfRequire = createRequire(createRequire(import.meta.url).resolve('mineflayer/package.json'));
|
|
169
|
+
const AABB = mfRequire('prismarine-physics/lib/aabb.js') as { prototype: PhysicsBox & { [WALL_GAP_MARK]?: true } };
|
|
170
|
+
const proto = AABB.prototype;
|
|
171
|
+
if (proto[WALL_GAP_MARK]) return;
|
|
172
|
+
// this 是方块的盒子,other 是正在挪的实体的盒子(prismarine-physics 这么调)
|
|
173
|
+
proto.computeOffsetX = function (this: PhysicsBox, other: PhysicsBox, offsetX: number): number {
|
|
174
|
+
if (other.maxY > this.minY && other.minY < this.maxY && other.maxZ > this.minZ && other.minZ < this.maxZ) {
|
|
175
|
+
if (offsetX > 0 && other.maxX <= this.minX + WALL_GAP) {
|
|
176
|
+
offsetX = Math.min(this.minX - other.maxX - WALL_GAP, offsetX);
|
|
177
|
+
} else if (offsetX < 0 && other.minX >= this.maxX - WALL_GAP) {
|
|
178
|
+
offsetX = Math.max(this.maxX - other.minX + WALL_GAP, offsetX);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return offsetX;
|
|
182
|
+
};
|
|
183
|
+
proto.computeOffsetZ = function (this: PhysicsBox, other: PhysicsBox, offsetZ: number): number {
|
|
184
|
+
if (other.maxX > this.minX && other.minX < this.maxX && other.maxY > this.minY && other.minY < this.maxY) {
|
|
185
|
+
if (offsetZ > 0 && other.maxZ <= this.minZ + WALL_GAP) {
|
|
186
|
+
offsetZ = Math.min(this.minZ - other.maxZ - WALL_GAP, offsetZ);
|
|
187
|
+
} else if (offsetZ < 0 && other.minZ >= this.maxZ - WALL_GAP) {
|
|
188
|
+
offsetZ = Math.max(this.maxZ - other.minZ + WALL_GAP, offsetZ);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return offsetZ;
|
|
192
|
+
};
|
|
193
|
+
proto[WALL_GAP_MARK] = true;
|
|
140
194
|
}
|
|
141
195
|
|
|
142
196
|
/**
|
|
@@ -178,6 +178,9 @@ export function renderSegmentsPlain(segments: Segment[]): string {
|
|
|
178
178
|
case 'image':
|
|
179
179
|
parts.push({ text: '[图片]', isToken: true });
|
|
180
180
|
break;
|
|
181
|
+
case 'record':
|
|
182
|
+
parts.push({ text: '[语音]', isToken: true });
|
|
183
|
+
break;
|
|
181
184
|
case 'face': {
|
|
182
185
|
const name = QFACE_NAMES[String(data.id ?? '')];
|
|
183
186
|
parts.push({ text: name ? `[表情:${name}]` : '[QQ表情]', isToken: true });
|
|
@@ -253,6 +256,11 @@ export function renderIncoming(
|
|
|
253
256
|
parts.push({ text: renderImage(data), isToken: true });
|
|
254
257
|
break;
|
|
255
258
|
}
|
|
259
|
+
// 语音条的音频不随消息到达,取回它要另一次动作调用;这里只说这是一条语音。
|
|
260
|
+
case 'record': {
|
|
261
|
+
parts.push({ text: '[语音]', isToken: true });
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
256
264
|
case 'face': {
|
|
257
265
|
const name = QFACE_NAMES[String(data.id ?? '')];
|
|
258
266
|
parts.push({ text: name ? `[表情:${name}]` : '[QQ表情]', isToken: true });
|
|
@@ -301,14 +309,20 @@ interface OutgoingArgs {
|
|
|
301
309
|
reply_to_message_id?: number | string;
|
|
302
310
|
/** 图片base64(不含data:前缀);有则编译成image段(OneBot base64://) */
|
|
303
311
|
image_base64?: string;
|
|
312
|
+
/** 语音base64(不含data:前缀);有则整条消息只有这一个record段 */
|
|
313
|
+
record_base64?: string;
|
|
304
314
|
}
|
|
305
315
|
|
|
306
316
|
/**
|
|
307
317
|
* 出站 → segment数组:reply段(如有)在前,然后image段(如有),最后text段
|
|
308
318
|
* (text非空才加——支持只发图不带字)。
|
|
319
|
+
* 语音条独占一条消息:QQ 的 record 段不与别的段同发,给了它就只发它。
|
|
309
320
|
* 不做 @ 编译:agent 写出的“@某人”作为纯文本原样发出。
|
|
310
321
|
*/
|
|
311
322
|
export function buildOutgoing(args: OutgoingArgs): Segment[] {
|
|
323
|
+
if (args.record_base64) {
|
|
324
|
+
return [{ type: 'record', data: { file: `base64://${args.record_base64}` } }];
|
|
325
|
+
}
|
|
312
326
|
const segments: Segment[] = [];
|
|
313
327
|
if (args.reply_to_message_id !== undefined) {
|
|
314
328
|
segments.push({ type: 'reply', data: { id: String(args.reply_to_message_id) } });
|
package/src/worlds/qq/world.ts
CHANGED
|
@@ -109,6 +109,8 @@ interface PendingDraft {
|
|
|
109
109
|
replyMessageId?: number | string;
|
|
110
110
|
/** 要发送的图片的句柄(`log:` 她看见过的 / `mem:` 她收藏的);无=纯文本 */
|
|
111
111
|
imageRef?: string;
|
|
112
|
+
/** 要发送的语音的句柄;语音条独占一条消息,与 text、imageRef 互斥 */
|
|
113
|
+
voiceRef?: string;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
interface ForwardNode {
|
|
@@ -130,7 +132,9 @@ export class QQWorld implements World {
|
|
|
130
132
|
private host?: WorldHost;
|
|
131
133
|
private driver?: OneBotDriver;
|
|
132
134
|
private log: Logger = nullLogger();
|
|
133
|
-
|
|
135
|
+
/** 主模型支持 image/png 或 World 配置了视觉模型时启用取图;每次渲染按当前端点判断。 */
|
|
136
|
+
private readonly imagePolicy: ImageRenderPolicy = (data) =>
|
|
137
|
+
makeImagePolicy(!!this.vision || (this.host?.modelFacts.accepts('image/png') ?? false))(data);
|
|
134
138
|
|
|
135
139
|
/** 外挂视觉(注入=auxVLM生效);null=无,一切保持[图片]占位现状 */
|
|
136
140
|
private readonly vision?: VisionService;
|
|
@@ -546,8 +550,6 @@ export class QQWorld implements World {
|
|
|
546
550
|
async start(host: WorldHost): Promise<void> {
|
|
547
551
|
this.host = host;
|
|
548
552
|
this.log = host.log;
|
|
549
|
-
// 主模型支持 image/png 或 World 配置了视觉模型时启用取图。
|
|
550
|
-
this.imagePolicy = makeImagePolicy(!!this.vision || host.modelFacts.accepts('image/png'));
|
|
551
553
|
// 外挂视觉的用量自愿上报进 core 的成本账(不报就在成本页看不到)
|
|
552
554
|
this.vision?.setUsageSink((usage, model) =>
|
|
553
555
|
host.reportUsage(usage, { model, label: 'QQWorld·辅助视觉' }),
|
|
@@ -1235,7 +1237,14 @@ export class QQWorld implements World {
|
|
|
1235
1237
|
/** 实际发送到目标会话,回录 qq.self,返回回执文本 */
|
|
1236
1238
|
private async sendToTarget(
|
|
1237
1239
|
target: Conv,
|
|
1238
|
-
opts: {
|
|
1240
|
+
opts: {
|
|
1241
|
+
text: string;
|
|
1242
|
+
replyMessageId?: number | string;
|
|
1243
|
+
imageRef?: string;
|
|
1244
|
+
imageBytes?: Uint8Array;
|
|
1245
|
+
voiceRef?: string;
|
|
1246
|
+
voiceBytes?: Uint8Array;
|
|
1247
|
+
},
|
|
1239
1248
|
): Promise<string> {
|
|
1240
1249
|
const host = this.host!;
|
|
1241
1250
|
const driver = this.driver!;
|
|
@@ -1247,6 +1256,7 @@ export class QQWorld implements World {
|
|
|
1247
1256
|
text: opts.text,
|
|
1248
1257
|
reply_to_message_id: opts.replyMessageId,
|
|
1249
1258
|
image_base64: imageBase64,
|
|
1259
|
+
...(opts.voiceBytes ? { record_base64: Buffer.from(opts.voiceBytes).toString('base64') } : {}),
|
|
1250
1260
|
});
|
|
1251
1261
|
const action = target.kind === 'group' ? 'send_group_msg' : 'send_private_msg';
|
|
1252
1262
|
const params =
|
|
@@ -1259,7 +1269,13 @@ export class QQWorld implements World {
|
|
|
1259
1269
|
// 回录正文:文字 + (如有)图片标记,保证自己发的图在历史里也可见。
|
|
1260
1270
|
// 行首保留平台消息号,使历史查询结果可引用自身消息。
|
|
1261
1271
|
const imgMark = opts.imageRef ? `[图片: ${opts.imageRef}]` : '';
|
|
1262
|
-
const
|
|
1272
|
+
const voiceMark = opts.voiceRef ? `[语音: ${opts.voiceRef}]` : '';
|
|
1273
|
+
const selfBody = [opts.text, imgMark, voiceMark].filter(Boolean).join(' ');
|
|
1274
|
+
const selfBlob = opts.imageRef
|
|
1275
|
+
? { handle: opts.imageRef, fallbackText: '你发出的图片' }
|
|
1276
|
+
: opts.voiceRef
|
|
1277
|
+
? { handle: opts.voiceRef, fallbackText: '你发出的语音' }
|
|
1278
|
+
: null;
|
|
1263
1279
|
const idTag = messageId !== undefined ? `#${messageId} ` : '';
|
|
1264
1280
|
const env = await host.pushEvent(
|
|
1265
1281
|
{
|
|
@@ -1267,8 +1283,8 @@ export class QQWorld implements World {
|
|
|
1267
1283
|
ts: nowIso(this.timezone),
|
|
1268
1284
|
source: this.id,
|
|
1269
1285
|
text: `${idTag}[${this.convLabel(target)} ${shortTime(this.timezone)}] 你: ${selfBody}`,
|
|
1270
|
-
//
|
|
1271
|
-
...(
|
|
1286
|
+
// 发出去的图和语音按句柄附在自己这条记录上:时间线与吃这些类型的模型都看得到发了什么
|
|
1287
|
+
...(selfBlob ? { blobs: [selfBlob] } : {}),
|
|
1272
1288
|
senderKey: String(driver.identity?.selfId ?? 'self'),
|
|
1273
1289
|
meta: {
|
|
1274
1290
|
message_id: messageId,
|
|
@@ -1320,6 +1336,13 @@ export class QQWorld implements World {
|
|
|
1320
1336
|
'Optional. An image to send, given by handle: a log: handle from a [blob ...] line you saw, '
|
|
1321
1337
|
+ 'or a mem: handle of one kept in your workspace.',
|
|
1322
1338
|
},
|
|
1339
|
+
voice: {
|
|
1340
|
+
type: 'string',
|
|
1341
|
+
description:
|
|
1342
|
+
'Optional. Audio to send as a QQ voice bar, given by handle the same way as image. '
|
|
1343
|
+
+ 'A voice bar is a message of its own: leave text, image and reply_to out. '
|
|
1344
|
+
+ 'The protocol end converts the audio it accepts (wav, mp3) to what QQ plays.',
|
|
1345
|
+
},
|
|
1323
1346
|
reply_to: {
|
|
1324
1347
|
type: 'string',
|
|
1325
1348
|
description:
|
|
@@ -1335,7 +1358,12 @@ export class QQWorld implements World {
|
|
|
1335
1358
|
const rawText = typeof args.text === 'string' ? args.text : '';
|
|
1336
1359
|
const imageArg =
|
|
1337
1360
|
typeof args.image === 'string' && args.image.trim() ? args.image.trim() : undefined;
|
|
1338
|
-
|
|
1361
|
+
const voiceArg =
|
|
1362
|
+
typeof args.voice === 'string' && args.voice.trim() ? args.voice.trim() : undefined;
|
|
1363
|
+
if (!rawText.trim() && !imageArg && !voiceArg) return '[bad input] provide at least one of text, image or voice';
|
|
1364
|
+
if (voiceArg && (rawText.trim() || imageArg || (args.reply_to !== undefined && args.reply_to !== null && String(args.reply_to) !== ''))) {
|
|
1365
|
+
return '[bad input] a voice bar is a message of its own; it carries no text, image or quote-reply';
|
|
1366
|
+
}
|
|
1339
1367
|
// 纯空白正文视为不带字(避免只发图时混进一个空白 text 段)
|
|
1340
1368
|
const text = rawText.trim() ? rawText : '';
|
|
1341
1369
|
|
|
@@ -1373,7 +1401,17 @@ export class QQWorld implements World {
|
|
|
1373
1401
|
: `[will send image: ${imageArg}]`;
|
|
1374
1402
|
}
|
|
1375
1403
|
|
|
1376
|
-
|
|
1404
|
+
// 语音:同样按句柄确认取得到,MIME 必须是音频
|
|
1405
|
+
let voiceRef: string | undefined;
|
|
1406
|
+
if (voiceArg) {
|
|
1407
|
+
const got = host.blob(voiceArg);
|
|
1408
|
+
if (!got) return `[send failed] no blob for ${voiceArg}; use a log: handle from a [blob ...] line or a mem: handle from your workspace`;
|
|
1409
|
+
if (!got.mime.startsWith('audio/')) return `[send failed] ${voiceArg} is ${got.mime}, not audio`;
|
|
1410
|
+
voiceRef = voiceArg;
|
|
1411
|
+
previewLine = `[will send voice: ${voiceArg}]`;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
this.pendingDraft = { target, text, replyMessageId: rep.mid, imageRef, voiceRef };
|
|
1377
1415
|
|
|
1378
1416
|
// 排出当下已积、尚未投递的QQ会话事件(consume-once)。正文不能伪装成
|
|
1379
1417
|
// draft 工具结果:交回 loop 按常规投递,本轮下一次推理前它们就在眼前,
|
|
@@ -1382,7 +1420,7 @@ export class QQWorld implements World {
|
|
|
1382
1420
|
ctx.queueExternalEvents?.(drained);
|
|
1383
1421
|
|
|
1384
1422
|
const lines = [`[draft staged → ${this.targetDesc(target)}]`];
|
|
1385
|
-
lines.push(text.trim() ? `Text: ${text}` : '(image only, no text)');
|
|
1423
|
+
lines.push(text.trim() ? `Text: ${text}` : voiceRef ? '(voice bar, no text)' : '(image only, no text)');
|
|
1386
1424
|
if (previewLine) lines.push(previewLine);
|
|
1387
1425
|
lines.push(
|
|
1388
1426
|
'',
|
|
@@ -1433,11 +1471,19 @@ export class QQWorld implements World {
|
|
|
1433
1471
|
if (!got) return `[send failed] blob ${draft.imageRef} is gone (draft kept)`;
|
|
1434
1472
|
imageBytes = got.bytes;
|
|
1435
1473
|
}
|
|
1474
|
+
let voiceBytes: Uint8Array | undefined;
|
|
1475
|
+
if (draft.voiceRef) {
|
|
1476
|
+
const got = host.blob(draft.voiceRef);
|
|
1477
|
+
if (!got) return `[send failed] blob ${draft.voiceRef} is gone (draft kept)`;
|
|
1478
|
+
voiceBytes = got.bytes;
|
|
1479
|
+
}
|
|
1436
1480
|
const receipt = await this.sendToTarget(draft.target, {
|
|
1437
1481
|
text: draft.text,
|
|
1438
1482
|
replyMessageId: draft.replyMessageId,
|
|
1439
1483
|
imageRef: draft.imageRef,
|
|
1440
1484
|
imageBytes,
|
|
1485
|
+
voiceRef: draft.voiceRef,
|
|
1486
|
+
voiceBytes,
|
|
1441
1487
|
});
|
|
1442
1488
|
this.pendingDraft = null; // 成功才清;失败保留草稿,可再 confirm 重试
|
|
1443
1489
|
return receipt;
|
|
@@ -74,8 +74,7 @@ const MODEL_TEXT = {
|
|
|
74
74
|
selfLine: (time: string, text: string) => `[${time}] you: ${text}`,
|
|
75
75
|
inviteLine: (time: string, name: string, label: string, spokenBefore: boolean) =>
|
|
76
76
|
`[${time}] ${name} pressed the "${label}" button on the terminal.`
|
|
77
|
-
+ (spokenBefore ? '' : '
|
|
78
|
-
+ ' You may say hello and introduce yourself.',
|
|
77
|
+
+ (spokenBefore ? '' : ' No speech is recorded in the retained terminal history.'),
|
|
79
78
|
imageFallback: (from: string, i: number, total: number) => `image ${i}/${total} from ${from}`,
|
|
80
79
|
deliveredTo: (delivered: number, names: string[]) =>
|
|
81
80
|
`Sent to the chat channel on the console's "Terminal" page; ${delivered} connection${delivered === 1 ? '' : 's'} online right now`
|
|
@@ -631,11 +630,11 @@ export class TerminalWorld implements World {
|
|
|
631
630
|
this.sendJson(client.peer, { type: 'sys', text: t.unknownType(String(msg.type)) });
|
|
632
631
|
}
|
|
633
632
|
|
|
634
|
-
/**
|
|
633
|
+
/** 已保存的终端事件里有没有人说过话。 */
|
|
635
634
|
private spokenBefore(): boolean {
|
|
636
635
|
if (!this.host) return false;
|
|
637
636
|
return this.host.store
|
|
638
|
-
.range({ source: this.id
|
|
637
|
+
.range({ source: this.id })
|
|
639
638
|
.some((e) => e.type === 'terminal.message' || e.type === 'terminal.self');
|
|
640
639
|
}
|
|
641
640
|
|