@maiyunnet/kebab 9.10.0 → 9.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/doc/kebab-rag.md +185 -117
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/ai.d.ts +9 -1
- package/lib/ai.js +41 -0
- package/lib/core.d.ts +1 -1
- package/lib/text.d.ts +2 -1
- package/lib/text.js +22 -6
- package/lib/undici/response.js +7 -3
- package/package.json +1 -1
- package/sys/cmd.js +4 -0
- package/sys/master.js +6 -1
- package/sys/mod.js +12 -0
- package/www/example/ctr/test.js +127 -0
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.11.0';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/ai.d.ts
CHANGED
|
@@ -32,7 +32,11 @@ export declare enum ESERVICE {
|
|
|
32
32
|
/** --- 火山引擎中国大陆区 --- */
|
|
33
33
|
'VOLCN' = 8,
|
|
34
34
|
/** --- 火山引擎国际区 --- */
|
|
35
|
-
'VOLAS' = 9
|
|
35
|
+
'VOLAS' = 9,
|
|
36
|
+
/** --- OpenRouter --- */
|
|
37
|
+
'OPENROUTER' = 10,
|
|
38
|
+
/** --- OfoxAI --- */
|
|
39
|
+
'OFOX' = 11
|
|
36
40
|
}
|
|
37
41
|
/** --- 选项 --- */
|
|
38
42
|
export interface IOptions {
|
|
@@ -59,6 +63,10 @@ export declare class Ai {
|
|
|
59
63
|
chat(body: openai.default.Chat.Completions.ChatCompletionCreateParamsNonStreaming): Promise<openai.APIPromise<openai.default.Chat.ChatCompletion> | false>;
|
|
60
64
|
/** --- 创建流式对话 --- */
|
|
61
65
|
chat(body: openai.default.Chat.Completions.ChatCompletionCreateParamsStreaming): Promise<openai.APIPromise<streaming.Stream<openai.default.Chat.ChatCompletionChunk>> | false>;
|
|
66
|
+
/** --- 创建非流式 Response --- */
|
|
67
|
+
response(body: openai.default.Responses.ResponseCreateParamsNonStreaming): Promise<openai.APIPromise<openai.default.Responses.Response> | false>;
|
|
68
|
+
/** --- 创建流式 Response --- */
|
|
69
|
+
response(body: openai.default.Responses.ResponseCreateParamsStreaming): Promise<openai.APIPromise<streaming.Stream<openai.default.Responses.ResponseStreamEvent>> | false>;
|
|
62
70
|
/** --- 创建向量 --- */
|
|
63
71
|
embedding(body: openai.default.EmbeddingCreateParams): Promise<openai.APIPromise<openai.default.CreateEmbeddingResponse> | false>;
|
|
64
72
|
/** --- 生成图像,不支持 GEMINI、GROK 服务商 --- */
|
package/lib/ai.js
CHANGED
|
@@ -34,6 +34,11 @@ export var ESERVICE;
|
|
|
34
34
|
ESERVICE[ESERVICE["VOLCN"] = 8] = "VOLCN";
|
|
35
35
|
/** --- 火山引擎国际区 --- */
|
|
36
36
|
ESERVICE[ESERVICE["VOLAS"] = 9] = "VOLAS";
|
|
37
|
+
// --- 以下为第三方服务商 ---
|
|
38
|
+
/** --- OpenRouter --- */
|
|
39
|
+
ESERVICE[ESERVICE["OPENROUTER"] = 10] = "OPENROUTER";
|
|
40
|
+
/** --- OfoxAI --- */
|
|
41
|
+
ESERVICE[ESERVICE["OFOX"] = 11] = "OFOX";
|
|
37
42
|
})(ESERVICE || (ESERVICE = {}));
|
|
38
43
|
/** --- openai 的连接对象 --- */
|
|
39
44
|
const links = [];
|
|
@@ -94,6 +99,14 @@ export class Ai {
|
|
|
94
99
|
endpoint = opt.endpoint ?? `https://ark.ap-southeast.bytepluses.com/api/v3`;
|
|
95
100
|
break;
|
|
96
101
|
}
|
|
102
|
+
case ESERVICE.OPENROUTER: {
|
|
103
|
+
endpoint = opt.endpoint ?? `https://openrouter.ai/api/v1`;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case ESERVICE.OFOX: {
|
|
107
|
+
endpoint = opt.endpoint ?? `https://api.ofox.ai/v1`;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
97
110
|
default: {
|
|
98
111
|
// --- ESERVICE.AZURE3 ---
|
|
99
112
|
endpoint = opt.endpoint ?? configAi.endpoint ?? '';
|
|
@@ -136,6 +149,19 @@ export class Ai {
|
|
|
136
149
|
return false;
|
|
137
150
|
}
|
|
138
151
|
}
|
|
152
|
+
/** --- 创建 Response --- */
|
|
153
|
+
async response(body) {
|
|
154
|
+
try {
|
|
155
|
+
return await this.link.responses.create(body);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
if (!e.message.includes('Input data may contain inappropriate content')) {
|
|
159
|
+
lCore.debug('[AI][RESPONSE]', e);
|
|
160
|
+
lCore.log(this._ctr ?? {}, `[AI][RESPONSE] ${e.message}`, '-error');
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
139
165
|
/** --- 创建向量 --- */
|
|
140
166
|
async embedding(body) {
|
|
141
167
|
try {
|
|
@@ -151,6 +177,9 @@ export class Ai {
|
|
|
151
177
|
}
|
|
152
178
|
/** --- 生成图像,不支持 GEMINI、GROK 服务商 --- */
|
|
153
179
|
async image(opt) {
|
|
180
|
+
if (!this.link.apiKey) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
154
183
|
const seed = opt.seed ?? lCore.rand(0, 2147483647);
|
|
155
184
|
switch (this._service) {
|
|
156
185
|
case ESERVICE.ALICN:
|
|
@@ -300,6 +329,12 @@ export class Ai {
|
|
|
300
329
|
return false;
|
|
301
330
|
}
|
|
302
331
|
}
|
|
332
|
+
case ESERVICE.OPENROUTER: {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
case ESERVICE.OFOX: {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
303
338
|
}
|
|
304
339
|
}
|
|
305
340
|
/** --- 异步生成视频,仅支持 ALICN、ALIAS、ALINE --- */
|
|
@@ -307,6 +342,9 @@ export class Ai {
|
|
|
307
342
|
if (this._service !== ESERVICE.ALICN && this._service !== ESERVICE.ALIAS && this._service !== ESERVICE.ALINE) {
|
|
308
343
|
return false;
|
|
309
344
|
}
|
|
345
|
+
if (!this.link.apiKey) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
310
348
|
const mode = opt.mode ?? 'text';
|
|
311
349
|
const imgs = opt.imgs ?? [];
|
|
312
350
|
const resolution = opt.resolution ?? '720p';
|
|
@@ -442,6 +480,9 @@ export class Ai {
|
|
|
442
480
|
if (this._service !== ESERVICE.ALICN && this._service !== ESERVICE.ALIAS && this._service !== ESERVICE.ALINE) {
|
|
443
481
|
return false;
|
|
444
482
|
}
|
|
483
|
+
if (!this.link.apiKey) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
445
486
|
try {
|
|
446
487
|
const res = await this._fetch(`https://dashscope${this._service === ESERVICE.ALIAS ? '-intl' : (this._service === ESERVICE.ALINE ? '-us' : '')}.aliyuncs.com/api/v1/tasks/${opt.task}`, {
|
|
447
488
|
'method': 'GET',
|
package/lib/core.d.ts
CHANGED
|
@@ -299,7 +299,7 @@ export declare function getLog(opt: {
|
|
|
299
299
|
* @param opt 参数
|
|
300
300
|
*/
|
|
301
301
|
export declare function ls(opt: {
|
|
302
|
-
/** --- 如 2024/08/01/22
|
|
302
|
+
/** --- 如 2024/08/01/22,无所谓开头结尾是否有 /,不会逃逸出 cwd 路径 --- */
|
|
303
303
|
'path': string;
|
|
304
304
|
'encoding'?: BufferEncoding;
|
|
305
305
|
/** --- 获取局域网服务器的目录列表,为空代表获取本机的 --- */
|
package/lib/text.d.ts
CHANGED
|
@@ -28,8 +28,9 @@ export declare function parseUrl(url: string): kebab.IUrlParse;
|
|
|
28
28
|
* --- 将相对路径根据基准路径进行转换 ---
|
|
29
29
|
* @param from 基准路径
|
|
30
30
|
* @param to 相对路径
|
|
31
|
+
* @param limit 是否限定结果不能逃逸出基准路径
|
|
31
32
|
*/
|
|
32
|
-
export declare function urlResolve(from: string, to: string): string;
|
|
33
|
+
export declare function urlResolve(from: string, to: string, limit?: boolean): string;
|
|
33
34
|
/**
|
|
34
35
|
* --- 将路径中的 ../ ./ 都按规范妥善处理 ---
|
|
35
36
|
* @param url 要处理的地址
|
package/lib/text.js
CHANGED
|
@@ -117,14 +117,30 @@ export function parseUrl(url) {
|
|
|
117
117
|
rtn['path'] = rtn['pathname'] + (rtn['query'] ? '?' + rtn['query'] : '');
|
|
118
118
|
return rtn;
|
|
119
119
|
}
|
|
120
|
+
/** --- 限定结果不能逃逸出基准路径 --- */
|
|
121
|
+
function urlResolveLimit(from, limit, rtn) {
|
|
122
|
+
if (!limit) {
|
|
123
|
+
return rtn;
|
|
124
|
+
}
|
|
125
|
+
const base = urlAtom(from).replace(/\/+$/, '') + '/';
|
|
126
|
+
if ((rtn === base.slice(0, -1)) || rtn.startsWith(base)) {
|
|
127
|
+
return rtn;
|
|
128
|
+
}
|
|
129
|
+
return base;
|
|
130
|
+
}
|
|
120
131
|
/**
|
|
121
132
|
* --- 将相对路径根据基准路径进行转换 ---
|
|
122
133
|
* @param from 基准路径
|
|
123
134
|
* @param to 相对路径
|
|
135
|
+
* @param limit 是否限定结果不能逃逸出基准路径
|
|
124
136
|
*/
|
|
125
|
-
export function urlResolve(from, to) {
|
|
126
|
-
from = from.replace(
|
|
127
|
-
to = to.replace(
|
|
137
|
+
export function urlResolve(from, to, limit = false) {
|
|
138
|
+
from = from.replace(/\\/g, '/');
|
|
139
|
+
to = to.replace(/\\/g, '/');
|
|
140
|
+
if (limit) {
|
|
141
|
+
// --- 限定模式下,开头的 / 代表从 from 内部开始,而不是系统根目录 ---
|
|
142
|
+
to = to.replace(/^\/+/, '');
|
|
143
|
+
}
|
|
128
144
|
// --- to 为空,直接返回 form ---
|
|
129
145
|
if (to === '') {
|
|
130
146
|
return urlAtom(from);
|
|
@@ -144,7 +160,7 @@ export function urlResolve(from, to) {
|
|
|
144
160
|
// --- 已经是绝对路径,直接返回 ---
|
|
145
161
|
if (t.protocol) {
|
|
146
162
|
// --- 获取小写的 protocol ---
|
|
147
|
-
return urlAtom(t.protocol + to.slice(t.protocol.length));
|
|
163
|
+
return urlResolveLimit(from, limit, urlAtom(t.protocol + to.slice(t.protocol.length)));
|
|
148
164
|
}
|
|
149
165
|
// --- # 或 ? 替换后返回 ---
|
|
150
166
|
if (to.startsWith('#') || to.startsWith('?')) {
|
|
@@ -172,11 +188,11 @@ export function urlResolve(from, to) {
|
|
|
172
188
|
// --- 返回最终结果 ---
|
|
173
189
|
if (f.protocol && (f.protocol !== 'file:') && !f.host) {
|
|
174
190
|
// --- 类似 c:/ ---
|
|
175
|
-
return urlAtom(f.protocol + abs);
|
|
191
|
+
return urlResolveLimit(from, limit, urlAtom(f.protocol + abs));
|
|
176
192
|
}
|
|
177
193
|
else {
|
|
178
194
|
// --- 类似 http:// ---
|
|
179
|
-
return urlAtom((f.protocol ? f.protocol + '//' : '') + abs);
|
|
195
|
+
return urlResolveLimit(from, limit, urlAtom((f.protocol ? f.protocol + '//' : '') + abs));
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
/**
|
package/lib/undici/response.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as zlib from 'zlib';
|
|
2
2
|
import * as lBuffer from '#kebab/lib/buffer.js';
|
|
3
|
+
import * as lCore from '#kebab/lib/core.js';
|
|
3
4
|
import * as lText from '#kebab/lib/text.js';
|
|
4
5
|
export class Response {
|
|
5
6
|
/** --- httpClient 请求对象 --- */
|
|
@@ -26,7 +27,8 @@ export class Response {
|
|
|
26
27
|
}
|
|
27
28
|
return this._req ? await lBuffer.getFull(stream) : null;
|
|
28
29
|
}
|
|
29
|
-
catch {
|
|
30
|
+
catch (e) {
|
|
31
|
+
lCore.log({}, '[Undici][Response][getContent] ' + e.message, '-error');
|
|
30
32
|
return null;
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -39,7 +41,8 @@ export class Response {
|
|
|
39
41
|
}
|
|
40
42
|
return buf.toString('utf-8');
|
|
41
43
|
}
|
|
42
|
-
catch {
|
|
44
|
+
catch (e) {
|
|
45
|
+
lCore.log({}, '[Undici][Response][getText] ' + e.message, '-error');
|
|
43
46
|
return null;
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -52,7 +55,8 @@ export class Response {
|
|
|
52
55
|
}
|
|
53
56
|
return lText.parseJson(text);
|
|
54
57
|
}
|
|
55
|
-
catch {
|
|
58
|
+
catch (e) {
|
|
59
|
+
lCore.log({}, '[Undici][Response][getJson] ' + e.message, '-error');
|
|
56
60
|
return null;
|
|
57
61
|
}
|
|
58
62
|
}
|
package/package.json
CHANGED
package/sys/cmd.js
CHANGED
|
@@ -191,6 +191,10 @@ async function run() {
|
|
|
191
191
|
config.ai['VOLCN'].skey ??= '';
|
|
192
192
|
config.ai['VOLAS'] ??= {};
|
|
193
193
|
config.ai['VOLAS'].skey ??= '';
|
|
194
|
+
config.ai['OPENROUTER'] ??= {};
|
|
195
|
+
config.ai['OPENROUTER'].skey ??= '';
|
|
196
|
+
config.ai['OFOX'] ??= {};
|
|
197
|
+
config.ai['OFOX'].skey ??= '';
|
|
194
198
|
// --- config - vector ---
|
|
195
199
|
config.vector ??= {};
|
|
196
200
|
config.vector.host ??= '127.0.0.1';
|
package/sys/master.js
CHANGED
|
@@ -386,6 +386,7 @@ function createRpcListener() {
|
|
|
386
386
|
'.nyc_output',
|
|
387
387
|
'tmp',
|
|
388
388
|
'temp',
|
|
389
|
+
'AGENTS.md',
|
|
389
390
|
]);
|
|
390
391
|
for (const path in ls) {
|
|
391
392
|
/** --- 带 / 开头的 zip 中文件完整路径,例如 "/www/pika/ctr/api.js" --- */
|
|
@@ -737,7 +738,11 @@ function createRpcListener() {
|
|
|
737
738
|
}
|
|
738
739
|
case 'ls': {
|
|
739
740
|
// --- 获取目录内文件/文件夹列表 ---
|
|
740
|
-
|
|
741
|
+
if (typeof msg.path !== 'string') {
|
|
742
|
+
res.end('Invalid path');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const path = lText.urlResolve(kebab.ROOT_CWD, msg.path, true);
|
|
741
746
|
res.end(lText.stringifyJson({
|
|
742
747
|
'result': 1,
|
|
743
748
|
'data': (await lFs.readDir(path, msg.encoding)).map(item => ({
|
package/sys/mod.js
CHANGED
|
@@ -681,6 +681,10 @@ export default class Mod {
|
|
|
681
681
|
*/
|
|
682
682
|
async refresh(lock = false) {
|
|
683
683
|
const cstr = this.constructor;
|
|
684
|
+
if (!this._data[cstr._$primary]) {
|
|
685
|
+
lCore.log(this._ctr ?? {}, '[MOD][refresh] primary key not found', '-error');
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
684
688
|
this._sql.select('*', cstr._$table + (this._index ? ('_' + this._index[0]) : '')).where([{
|
|
685
689
|
[cstr._$primary]: this._data[cstr._$primary]
|
|
686
690
|
}]);
|
|
@@ -711,6 +715,10 @@ export default class Mod {
|
|
|
711
715
|
return true;
|
|
712
716
|
}
|
|
713
717
|
const cstr = this.constructor;
|
|
718
|
+
if (!this._data[cstr._$primary]) {
|
|
719
|
+
lCore.log(this._ctr ?? {}, '[MOD][save] primary key not found', '-error');
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
714
722
|
const updates = {};
|
|
715
723
|
for (const k in this._updates) {
|
|
716
724
|
updates[k] = this._data[k];
|
|
@@ -736,6 +744,10 @@ export default class Mod {
|
|
|
736
744
|
*/
|
|
737
745
|
async remove() {
|
|
738
746
|
const cstr = this.constructor;
|
|
747
|
+
if (!this._data[cstr._$primary]) {
|
|
748
|
+
lCore.log(this._ctr ?? {}, '[MOD][remove] primary key not found', '-error');
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
739
751
|
this._sql.delete(cstr._$table + (this._index ? ('_' + this._index[0]) : '')).where([{
|
|
740
752
|
[cstr._$primary]: this._data[cstr._$primary]
|
|
741
753
|
}]);
|
package/www/example/ctr/test.js
CHANGED
|
@@ -140,6 +140,7 @@ export default class extends sCtr.Ctr {
|
|
|
140
140
|
`<br><br><b>Ai:</b>`,
|
|
141
141
|
`<br><a href="${this._config.const.urlBase}test/ai-stream">View "test/ai-stream"</a>`,
|
|
142
142
|
`<br><a href="${this._config.const.urlBase}test/ai?action=chat">View "test/ai?action=chat"</a>`,
|
|
143
|
+
`<br><a href="${this._config.const.urlBase}test/ai?action=response">View "test/ai?action=response"</a>`,
|
|
143
144
|
`<br><a href="${this._config.const.urlBase}test/ai?action=text-to-image">View "test/ai?action=text-to-image"</a>`,
|
|
144
145
|
`<br><a href="${this._config.const.urlBase}test/ai?action=image-to-image">View "test/ai?action=image-to-image"</a>`,
|
|
145
146
|
`<br><a href="${this._config.const.urlBase}test/ai?action=text-to-video">View "test/ai?action=text-to-video"</a>`,
|
|
@@ -3692,6 +3693,8 @@ ${lText.htmlescape(lText.urlResolve('C:\\Windows\\Misc', '/'))}
|
|
|
3692
3693
|
${lText.htmlescape(lText.urlResolve('C:\\Windows\\Misc', '/xxx/yyy'))}
|
|
3693
3694
|
<pre>lText.urlResolve('/abc/def/', '');</pre>
|
|
3694
3695
|
${lText.htmlescape(lText.urlResolve('/abc/def/', ''))}
|
|
3696
|
+
<pre>lText.urlResolve('/abc/def/', '../1/2/3', true);</pre>
|
|
3697
|
+
${lText.htmlescape(lText.urlResolve('/abc/def/', '../1/2/3', true))}
|
|
3695
3698
|
<pre>lText.isEMail('test@gmail.com');</pre>
|
|
3696
3699
|
${JSON.stringify(lText.isEMail('test@gmail.com'))}
|
|
3697
3700
|
<pre>lText.isEMail('test@x');</pre>
|
|
@@ -4093,11 +4096,13 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
|
|
|
4093
4096
|
return echo.join('') + '<br>' + this._getEnd();
|
|
4094
4097
|
}
|
|
4095
4098
|
async ai() {
|
|
4099
|
+
this.timeout = 60_000 * 10;
|
|
4096
4100
|
const echo = [`<pre>const ai = lAi.get(this, {
|
|
4097
4101
|
'service': lAi.ESERVICE.${lText.htmlescape(this._get['service']?.toUpperCase() ?? 'ALICN')},
|
|
4098
4102
|
});</pre>`];
|
|
4099
4103
|
const ai = lAi.get(this, {
|
|
4100
4104
|
'service': lAi.ESERVICE[this._get['service']?.toUpperCase() ?? 'ALICN'] ?? lAi.ESERVICE.ALICN,
|
|
4105
|
+
'endpoint': this._get['service'] === 'ofox' ? 'https://api.ofox.io/v1' : undefined,
|
|
4101
4106
|
});
|
|
4102
4107
|
switch (this._get['action']) {
|
|
4103
4108
|
case 'text-to-image': {
|
|
@@ -4241,6 +4246,128 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
|
|
|
4241
4246
|
}
|
|
4242
4247
|
break;
|
|
4243
4248
|
}
|
|
4249
|
+
case 'response': {
|
|
4250
|
+
// --- Responses API:多轮对话 + Function Calling + 数据库存储方案 ---
|
|
4251
|
+
let model = ai.service === lAi.ESERVICE.ALICN ? 'qwen3.5-flash' : 'openai/gpt-5-nano';
|
|
4252
|
+
const responseInstructions = 'You are Kebab, a helpful assistant. Do not mention any model names or AI identity. Respond in Chinese.';
|
|
4253
|
+
// --- 工具定义 ---
|
|
4254
|
+
const responseTools = [
|
|
4255
|
+
{
|
|
4256
|
+
'type': 'function',
|
|
4257
|
+
'name': 'get_weather',
|
|
4258
|
+
'description': '获取指定城市的当前天气,返回 `false`: 调用失败, `null`: 无数据, 成功: 温度和天气',
|
|
4259
|
+
'parameters': {
|
|
4260
|
+
'type': 'object',
|
|
4261
|
+
'required': ['city'],
|
|
4262
|
+
'properties': {
|
|
4263
|
+
'city': { 'type': 'string', 'description': '城市名称,如"北京"' },
|
|
4264
|
+
},
|
|
4265
|
+
},
|
|
4266
|
+
},
|
|
4267
|
+
];
|
|
4268
|
+
// --- 模拟本地工具执行 ---
|
|
4269
|
+
const execTool = (name, argsJson) => {
|
|
4270
|
+
const args = lText.parseJson(argsJson);
|
|
4271
|
+
if (!args) {
|
|
4272
|
+
return 'false';
|
|
4273
|
+
}
|
|
4274
|
+
if (name === 'get_weather') {
|
|
4275
|
+
const table = {
|
|
4276
|
+
'北京': { 'temp': '22°C', 'condition': '晴' },
|
|
4277
|
+
'上海': { 'temp': '28°C', 'condition': '多云' },
|
|
4278
|
+
};
|
|
4279
|
+
return table[args['city']] ? lText.stringifyJson(table[args['city']]) : 'null';
|
|
4280
|
+
}
|
|
4281
|
+
return 'false';
|
|
4282
|
+
};
|
|
4283
|
+
// --- 完整对话历史(同时作为数据库存储内容) ---
|
|
4284
|
+
const conversation = [];
|
|
4285
|
+
// --- 运行一轮(自动处理 function_call 循环) ---
|
|
4286
|
+
const runTurn = async (userMsg) => {
|
|
4287
|
+
conversation.push({ 'role': 'user', 'content': userMsg });
|
|
4288
|
+
let data = await ai.response({
|
|
4289
|
+
'model': model,
|
|
4290
|
+
'store': false,
|
|
4291
|
+
'reasoning': {
|
|
4292
|
+
'effort': 'minimal',
|
|
4293
|
+
},
|
|
4294
|
+
'instructions': responseInstructions,
|
|
4295
|
+
'input': conversation,
|
|
4296
|
+
'tools': responseTools,
|
|
4297
|
+
});
|
|
4298
|
+
while (data) {
|
|
4299
|
+
const calls = data.output.filter(item => item.type === 'function_call');
|
|
4300
|
+
if (!calls.length) {
|
|
4301
|
+
// --- 无工具调用,追加 assistant 回复,供下一轮使用 ---
|
|
4302
|
+
conversation.push({ 'role': 'assistant', 'content': data.output_text });
|
|
4303
|
+
return data;
|
|
4304
|
+
}
|
|
4305
|
+
// --- 将 function_call 追加到 conversation ---
|
|
4306
|
+
for (const call of calls) {
|
|
4307
|
+
conversation.push({
|
|
4308
|
+
'type': 'function_call',
|
|
4309
|
+
'name': call.name,
|
|
4310
|
+
'arguments': call.arguments,
|
|
4311
|
+
'call_id': call.call_id,
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4314
|
+
// --- 全部追加完后再统一执行工具,将结果追加到 conversation,再次调用 ---
|
|
4315
|
+
for (const call of calls) {
|
|
4316
|
+
conversation.push({
|
|
4317
|
+
'type': 'function_call_output',
|
|
4318
|
+
'call_id': call.call_id,
|
|
4319
|
+
'output': execTool(call.name, call.arguments),
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
data = await ai.response({
|
|
4323
|
+
'model': model,
|
|
4324
|
+
'store': false,
|
|
4325
|
+
'reasoning': {
|
|
4326
|
+
'effort': 'minimal',
|
|
4327
|
+
},
|
|
4328
|
+
'instructions': responseInstructions,
|
|
4329
|
+
'input': conversation,
|
|
4330
|
+
'tools': responseTools,
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
return false;
|
|
4334
|
+
};
|
|
4335
|
+
// --- 第一轮:自我介绍 + 查北京天气(触发 Function Calling) ---
|
|
4336
|
+
const d1str = '你是谁?今天北京天气怎么样?';
|
|
4337
|
+
const d1 = await runTurn(d1str);
|
|
4338
|
+
if (!d1) {
|
|
4339
|
+
echo.push('Turn 1 Failed');
|
|
4340
|
+
break;
|
|
4341
|
+
}
|
|
4342
|
+
echo.push(`<b>第一轮(触发 get_weather 工具):</b><br>${d1str + '<br>' + lText.htmlescape(d1.output_text)}<br><br>`);
|
|
4343
|
+
// --- 第二轮:续接上文,查上海天气 ---
|
|
4344
|
+
const d2str = '那上海呢?';
|
|
4345
|
+
const d2 = await runTurn(d2str);
|
|
4346
|
+
if (!d2) {
|
|
4347
|
+
echo.push('Turn 2 Failed');
|
|
4348
|
+
break;
|
|
4349
|
+
}
|
|
4350
|
+
echo.push(`<b>第二轮(conversation 续接,自动触发工具):</b><br>${d2str + '<br>' + lText.htmlescape(d2.output_text)}<br><br>`);
|
|
4351
|
+
// --- 第三轮:基于前两轮结果直接推理,无需工具 ---
|
|
4352
|
+
const d3str = '两个城市比较,今天哪个更适合出行?';
|
|
4353
|
+
const d3 = await runTurn(d3str);
|
|
4354
|
+
if (!d3) {
|
|
4355
|
+
echo.push('Turn 3 Failed');
|
|
4356
|
+
break;
|
|
4357
|
+
}
|
|
4358
|
+
echo.push(`<b>第三轮(直接推理):</b><br>${d3str + '<br>' + lText.htmlescape(d3.output_text)}<br>`);
|
|
4359
|
+
// --- 数据库存储格式 ---
|
|
4360
|
+
// --- conversation 顺序:user → function_call → function_call_output → assistant → user → ... ---
|
|
4361
|
+
echo.push(`<hr><b>数据库存储 input:</b><pre>${lText.htmlescape(lText.stringifyJson(conversation, 4))}</pre>`);
|
|
4362
|
+
// --- 从头重建(直接将 dbRecord.input 发送即可) ---
|
|
4363
|
+
echo.push(`<hr><b>从头重建:</b><pre>${lText.htmlescape(`const replay = await ai.response({
|
|
4364
|
+
'model': model,
|
|
4365
|
+
'instructions': instructions,
|
|
4366
|
+
'input': conversation,
|
|
4367
|
+
'tools': tools,
|
|
4368
|
+
});`)}</pre>`);
|
|
4369
|
+
break;
|
|
4370
|
+
}
|
|
4244
4371
|
default: {
|
|
4245
4372
|
// --- CHAT ---
|
|
4246
4373
|
let model = ai.service === lAi.ESERVICE.ALICN ? 'qwen-plus' : 'doubao-seed-2-0-mini-260215';
|