@baishuyun/chat-backend 0.0.4 → 0.0.7

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @baishuyun/chat-backend
2
2
 
3
+ ## 0.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - bump
8
+
9
+ ## 0.0.6
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+ - @baishuyun/coze-provider@0.0.7
15
+ - @baishuyun/types@1.0.7
16
+
17
+ ## 0.0.5
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - @baishuyun/coze-provider@0.0.6
23
+ - @baishuyun/types@1.0.6
24
+
3
25
  ## 0.0.4
4
26
 
5
27
  ### Patch Changes
package/config/index.ts CHANGED
@@ -1,14 +1,12 @@
1
- import path from "path";
2
- import dotenv from "dotenv";
3
- import { fileURLToPath } from "url";
1
+ import path from 'path';
2
+ import dotenv from 'dotenv';
3
+ import { fileURLToPath } from 'url';
4
4
 
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- // export const parseConfig = () => {
9
- if (process.env.NODE_ENV !== "production") {
10
- // 加载本地.env文件到process.env
11
- dotenv.config({
12
- path: path.resolve(__dirname, "../.env"),
13
- });
14
- }
8
+ const isProd = process.env.NODE_ENV === 'production';
9
+
10
+ dotenv.config({
11
+ path: path.resolve(__dirname, isProd ? '../../.env' : '../.env'),
12
+ });
@@ -1,12 +1,9 @@
1
- import path from "path";
2
- import dotenv from "dotenv";
3
- import { fileURLToPath } from "url";
1
+ import path from 'path';
2
+ import dotenv from 'dotenv';
3
+ import { fileURLToPath } from 'url';
4
4
  const __filename = fileURLToPath(import.meta.url);
5
5
  const __dirname = path.dirname(__filename);
6
- // export const parseConfig = () => {
7
- if (process.env.NODE_ENV !== "production") {
8
- // 加载本地.env文件到process.env
9
- dotenv.config({
10
- path: path.resolve(__dirname, "../.env"),
11
- });
12
- }
6
+ const isProd = process.env.NODE_ENV === 'production';
7
+ dotenv.config({
8
+ path: path.resolve(__dirname, isProd ? '../../.env' : '../.env'),
9
+ });
@@ -6,8 +6,8 @@ import app from '../config/hono.config.js';
6
6
  // 子路由
7
7
  import { createFormRouter } from '../routes/form/form.route.js';
8
8
  // 挂载子路由
9
- app.route('api/form', createFormRouter());
9
+ app.route('web/api/form', createFormRouter());
10
10
  // add test route
11
- app.get('/api/test', (c) => {
11
+ app.get('web/api/test', (c) => {
12
12
  return c.json({ message: 'Test route is working!' });
13
13
  });
@@ -1,15 +1,15 @@
1
- import { Hono } from "hono";
2
- import { serve } from "@hono/node-server";
3
- import config from "config";
4
- import { cors } from "hono/cors";
5
- import { logMiddleware } from "../logger/log-middleware.js";
6
- import { logger } from "../logger/index.js";
1
+ import { Hono } from 'hono';
2
+ import { serve } from '@hono/node-server';
3
+ import config from 'config';
4
+ import { cors } from 'hono/cors';
5
+ import { logMiddleware } from '../logger/log-middleware.js';
6
+ import { logger } from '../logger/index.js';
7
7
  /**
8
8
  * 公共配置层,跨域、日志等中间件配置
9
9
  */
10
10
  const app = new Hono();
11
11
  // Enable CORS for all routes starting with /api/
12
- app.use("/api/*", cors());
12
+ app.use('web/api/*', cors());
13
13
  // Logging middleware
14
14
  app.use(logMiddleware);
15
15
  // 全局错误处理中间件(捕获所有路由/中间件异常)
@@ -20,14 +20,14 @@ app.onError((err, c) => {
20
20
  method: c.req.method,
21
21
  path: c.req.path,
22
22
  status: 500, // 默认 500 错误
23
- }, "Request failed with unhandled error"); // 错误描述
23
+ }, 'Request failed with unhandled error'); // 错误描述
24
24
  // 返回统一的 JSON 错误响应
25
- return c.json({ message: "Internal Server Error" }, 500);
25
+ return c.json({ message: 'Internal Server Error' }, 500);
26
26
  });
27
27
  serve({
28
28
  fetch: app.fetch,
29
- hostname: config.get("app.host") || "",
30
- port: config.get("app.port") || 3001,
29
+ hostname: config.get('app.host') || '',
30
+ port: config.get('app.port') || 3001,
31
31
  }, (info) => {
32
32
  logger.info(`Server is running on http://localhost:${info.port}`);
33
33
  });
@@ -19,14 +19,15 @@ export const buildForm = async (c) => {
19
19
  const isBuildStage = requestBody.stage === "build";
20
20
  const stream = streamText({
21
21
  model: createModel([
22
- createFieldsJsonTransformStream(isBuildStage),
23
- new SuggestionTransformStream(isBuildStage),
22
+ () => createFieldsJsonTransformStream(isBuildStage),
23
+ () => new SuggestionTransformStream(isBuildStage),
24
24
  ]),
25
25
  messages: convertToModelMessages(requestBody.messages),
26
26
  includeRawChunks: true,
27
27
  headers: {
28
28
  "x-user-stage": requestBody.stage,
29
29
  "x-user-id": Date.now().toString(),
30
+ "x-user-token": c.req.header("authorization") || "",
30
31
  },
31
32
  });
32
33
  return stream.toUIMessageStreamResponse();
@@ -0,0 +1,139 @@
1
+ import {} from 'hono';
2
+ import { convertToModelMessages, streamText, createUIMessageStream, createUIMessageStreamResponse, generateId, } from 'ai';
3
+ import { logger } from '../../../logger/index.js';
4
+ import { createBatchFillingModel } from './model.js';
5
+ import { mode2part } from './utils.js';
6
+ const getModelMessagesFromUserMessages = ({ continueMessageId, currentMsg, formStructure, mode, messages, }) => {
7
+ // init fill
8
+ if (continueMessageId && currentMsg) {
9
+ return convertToModelMessages([
10
+ {
11
+ role: 'user',
12
+ parts: [
13
+ {
14
+ type: 'text',
15
+ text: `formStructure: ${JSON.stringify(formStructure)}`,
16
+ },
17
+ mode2part(mode),
18
+ {
19
+ type: 'text',
20
+ text: currentMsg,
21
+ },
22
+ ],
23
+ },
24
+ ]);
25
+ }
26
+ let lastUserMsg = messages.findLast((m) => m.role === 'user') || {
27
+ id: generateId(),
28
+ role: 'user',
29
+ parts: [],
30
+ };
31
+ lastUserMsg.parts.push({
32
+ type: 'text',
33
+ text: `formStructure: ${JSON.stringify(formStructure)}`,
34
+ });
35
+ lastUserMsg.parts.push(mode2part(mode));
36
+ return convertToModelMessages([lastUserMsg]);
37
+ };
38
+ /**
39
+ * 表单填写
40
+ * @param c
41
+ * @returns
42
+ */
43
+ export const batchFillForm = async (c) => {
44
+ let requestBody;
45
+ try {
46
+ const json = await c.req.json();
47
+ requestBody = json;
48
+ }
49
+ catch (_) {
50
+ return c.json({ error: 'Invalid JSON' }, 400);
51
+ }
52
+ const formStructure = requestBody.formStructure;
53
+ const mode = requestBody.mode;
54
+ const continueMessageId = requestBody.continueMessageId; // 新增:继续生成的消息 ID
55
+ const userMessages = requestBody.messages;
56
+ // clear empty user text part message
57
+ const messages = userMessages.filter((msg) => {
58
+ if (msg.role !== 'user') {
59
+ return true;
60
+ }
61
+ const firstPart = msg.parts[0];
62
+ if (!firstPart) {
63
+ return false;
64
+ }
65
+ if (firstPart.type === 'text' && firstPart.text.trim() === '') {
66
+ return false;
67
+ }
68
+ return true;
69
+ });
70
+ // 如果是继续生成模式
71
+ if (continueMessageId) {
72
+ return handleContinueGeneration(c, {
73
+ messages,
74
+ formStructure,
75
+ continueMessageId,
76
+ msg: requestBody.msg,
77
+ mode,
78
+ stage: requestBody.stage,
79
+ });
80
+ }
81
+ const modelMessages = getModelMessagesFromUserMessages({
82
+ messages,
83
+ mode,
84
+ formStructure,
85
+ });
86
+ // 正常创建新流
87
+ const stream = streamText({
88
+ model: createBatchFillingModel(),
89
+ messages: modelMessages,
90
+ includeRawChunks: true,
91
+ headers: {
92
+ 'x-user-stage': requestBody.stage,
93
+ 'x-user-token': c.req.header('authorization') || '',
94
+ 'x-user-var': JSON.stringify({
95
+ mode: requestBody.mode,
96
+ }),
97
+ },
98
+ });
99
+ return stream.toUIMessageStreamResponse({
100
+ originalMessages: messages, // 建议添加,便于消息 ID 管理
101
+ });
102
+ };
103
+ /**
104
+ * 处理继续生成(扩展同一消息)
105
+ */
106
+ async function handleContinueGeneration(c, { messages, continueMessageId, mode, msg, formStructure, stage, }) {
107
+ const stream = createUIMessageStream({
108
+ execute: async ({ writer }) => {
109
+ writer.write({
110
+ type: 'start',
111
+ messageId: continueMessageId,
112
+ });
113
+ // 构建模型消息
114
+ const modelMessages = getModelMessagesFromUserMessages({
115
+ messages,
116
+ continueMessageId,
117
+ currentMsg: msg,
118
+ mode,
119
+ formStructure,
120
+ });
121
+ logger.debug(`continue modelMessages ${JSON.stringify(modelMessages)}`);
122
+ const result = streamText({
123
+ model: createBatchFillingModel(),
124
+ messages: modelMessages,
125
+ includeRawChunks: true,
126
+ headers: {
127
+ 'x-user-stage': stage,
128
+ 'x-user-var': JSON.stringify({
129
+ mode,
130
+ }),
131
+ },
132
+ });
133
+ writer.merge(result.toUIMessageStream({
134
+ sendStart: false,
135
+ }));
136
+ },
137
+ });
138
+ return createUIMessageStreamResponse({ stream });
139
+ }
@@ -0,0 +1,124 @@
1
+ import {} from '@ai-sdk/provider';
2
+ import { JSONParser } from '@streamparser/json';
3
+ import { createTextInfoEnqueuer } from '@baishuyun/coze-provider';
4
+ import { getLastSubFormField } from './utils.js';
5
+ import { fillingChunkGuard as chunkGuard } from './utils.js';
6
+ import { logger } from '../../../logger/index.js';
7
+ export const createBatchFillingResultTransformer = (enableJsonParser) => {
8
+ let parser;
9
+ let id;
10
+ // 解析完成标记:用于等待解析器内部队列处理完毕
11
+ let parseCompleted;
12
+ let resolveParseCompleted;
13
+ const transformer = {
14
+ flush: async (controller) => {
15
+ try {
16
+ if (parser) {
17
+ await parseCompleted;
18
+ logger.debug('stop parser');
19
+ }
20
+ controller.terminate(); // 信号通知下游可读流已关闭
21
+ }
22
+ catch (e) {
23
+ controller.error('stop error' + e);
24
+ // controller.terminate(); // 信号通知下游可读流已关闭
25
+ }
26
+ },
27
+ transform: (chunk, controller) => {
28
+ if (!parser) {
29
+ logger.warn('json parser not init');
30
+ return;
31
+ }
32
+ const enqueueTextDelta = createTextInfoEnqueuer(controller);
33
+ try {
34
+ if (chunkGuard(chunk)) {
35
+ if ('id' in chunk && 'delta' in chunk) {
36
+ id = chunk.id;
37
+ const regExp = /`?`?`?/;
38
+ let delta = chunk.delta.replace(regExp, '');
39
+ parser.write(delta.replace('json', ''));
40
+ }
41
+ }
42
+ else {
43
+ // logger.debug(`bypass chunk in batch filling transformer: ${JSON.stringify(chunk)}`);
44
+ controller.enqueue(chunk);
45
+ }
46
+ }
47
+ catch (e) {
48
+ logger.debug('exception in transform while processing filling chunk');
49
+ controller.error(e);
50
+ enqueueTextDelta('error', {
51
+ type: 'agent-error',
52
+ error: '解析异常,请刷新重试',
53
+ });
54
+ resolveParseCompleted?.();
55
+ controller.terminate(); // 信号通知下游可读流已关闭
56
+ }
57
+ },
58
+ start: (controller) => {
59
+ parser = new JSONParser({
60
+ paths: ['$.*', '$.*.value.*.*'],
61
+ });
62
+ parseCompleted = new Promise((resolve) => {
63
+ resolveParseCompleted = resolve;
64
+ });
65
+ const enqueueTextDelta = enableJsonParser
66
+ ? createTextInfoEnqueuer(controller)
67
+ : (content) => { };
68
+ parser.onValue = (parsedInfo) => {
69
+ const value = parsedInfo.value;
70
+ if (value.error && value.errorMessage === 'timeout') {
71
+ enqueueTextDelta(JSON.stringify(value), {
72
+ type: 'agent-error',
73
+ error: '操作超时,请刷新重试',
74
+ });
75
+ resolveParseCompleted?.();
76
+ controller.terminate(); // 信号通知下游可读流已关闭
77
+ return;
78
+ }
79
+ const subFormField = getLastSubFormField(parsedInfo);
80
+ if (subFormField) {
81
+ // enqueueTextDelta(
82
+ // JSON.stringify(value),
83
+ // {
84
+ // type: 'mcp-fields-json',
85
+ // field: subFormField.value,
86
+ // },
87
+ // id,
88
+ // true
89
+ // );
90
+ return;
91
+ }
92
+ if (value.fieldType === 'subform') {
93
+ return;
94
+ }
95
+ // logger.debug(`id in onValue: ${id}`);
96
+ enqueueTextDelta(`${JSON.stringify(value)},`, {
97
+ type: 'mcp-fields-json',
98
+ field: value,
99
+ }, id, true);
100
+ };
101
+ let errorLogged = false;
102
+ parser.onError = (err) => {
103
+ if (!errorLogged) {
104
+ console.error('JsonWidgetStream: JSON Parsing Error:', err);
105
+ errorLogged = true;
106
+ }
107
+ enqueueTextDelta('error', {
108
+ type: 'agent-error',
109
+ error: '操作超时,请刷新重试',
110
+ });
111
+ resolveParseCompleted?.();
112
+ controller.terminate(); // 信号通知下游可读流已关闭
113
+ };
114
+ parser.onEnd = () => {
115
+ enqueueTextDelta(' ', JSON.stringify({
116
+ appendConfirm: 'save-fields',
117
+ }));
118
+ logger.debug('mark as end');
119
+ resolveParseCompleted?.();
120
+ };
121
+ },
122
+ };
123
+ return new TransformStream(transformer);
124
+ };
@@ -2,6 +2,7 @@ import {} from '@ai-sdk/provider';
2
2
  import { JSONParser } from '@streamparser/json';
3
3
  import { createTextInfoEnqueuer } from '@baishuyun/coze-provider';
4
4
  import { getLastSubFormField } from './utils.js';
5
+ import { fillingChunkGuard as chunkGuard } from './utils.js';
5
6
  import { logger } from '../../../logger/index.js';
6
7
  export const createFieldsFillingResultTransformer = (enableJsonParser) => {
7
8
  let parser;
@@ -9,22 +10,12 @@ export const createFieldsFillingResultTransformer = (enableJsonParser) => {
9
10
  // 解析完成标记:用于等待解析器内部队列处理完毕
10
11
  let parseCompleted;
11
12
  let resolveParseCompleted;
12
- const chunkGuard = (chunk) => {
13
- if (!enableJsonParser) {
14
- return false;
15
- }
16
- if (chunk.type !== 'text-delta' || !('delta' in chunk)) {
17
- return false;
18
- }
19
- return true;
20
- };
21
13
  const transformer = {
22
14
  flush: async (controller) => {
23
15
  try {
24
16
  if (parser) {
25
- await parseCompleted; // 等待闭包中的完成标记
17
+ await parseCompleted;
26
18
  logger.debug('stop parser');
27
- // parser.end(); // 此时调用 end() 不会出现未完成 Token 异常
28
19
  }
29
20
  controller.terminate(); // 信号通知下游可读流已关闭
30
21
  }
@@ -38,6 +29,7 @@ export const createFieldsFillingResultTransformer = (enableJsonParser) => {
38
29
  logger.warn('json parser not init');
39
30
  return;
40
31
  }
32
+ const enqueueTextDelta = createTextInfoEnqueuer(controller);
41
33
  try {
42
34
  if (chunkGuard(chunk)) {
43
35
  if ('id' in chunk && 'delta' in chunk) {
@@ -52,7 +44,14 @@ export const createFieldsFillingResultTransformer = (enableJsonParser) => {
52
44
  }
53
45
  }
54
46
  catch (e) {
47
+ logger.debug('exception in transform while processing filling chunk');
55
48
  controller.error(e);
49
+ enqueueTextDelta('error', {
50
+ type: 'agent-error',
51
+ error: '解析异常,请刷新重试',
52
+ });
53
+ resolveParseCompleted?.();
54
+ controller.terminate(); // 信号通知下游可读流已关闭
56
55
  }
57
56
  },
58
57
  start: (controller) => {
@@ -67,6 +66,15 @@ export const createFieldsFillingResultTransformer = (enableJsonParser) => {
67
66
  : (content) => { };
68
67
  parser.onValue = (parsedInfo) => {
69
68
  const value = parsedInfo.value;
69
+ if (value.error && value.errorMessage === 'timeout') {
70
+ enqueueTextDelta(JSON.stringify(value), {
71
+ type: 'agent-error',
72
+ error: '操作超时,请刷新重试',
73
+ });
74
+ resolveParseCompleted?.();
75
+ controller.terminate(); // 信号通知下游可读流已关闭
76
+ return;
77
+ }
70
78
  const subFormField = getLastSubFormField(parsedInfo);
71
79
  if (subFormField) {
72
80
  enqueueTextDelta(JSON.stringify(value), {
@@ -85,16 +93,15 @@ export const createFieldsFillingResultTransformer = (enableJsonParser) => {
85
93
  };
86
94
  let errorLogged = false;
87
95
  parser.onError = (err) => {
88
- // controller.enqueue({
89
- // type: "error",
90
- // error: "JsonWidgetStream: JSON Parsing Error:" + err.message,
91
- // });
92
96
  if (!errorLogged) {
93
97
  console.error('JsonWidgetStream: JSON Parsing Error:', err);
94
98
  errorLogged = true;
95
99
  }
96
- // close stream
97
- logger.error('JsonStream: closing stream due to parsing error');
100
+ enqueueTextDelta('error', {
101
+ type: 'agent-error',
102
+ error: '操作超时,请刷新重试',
103
+ });
104
+ resolveParseCompleted?.();
98
105
  controller.terminate(); // 信号通知下游可读流已关闭
99
106
  };
100
107
  parser.onEnd = () => {
@@ -1,7 +1,9 @@
1
1
  import {} from 'hono';
2
- import { convertToModelMessages, streamText } from 'ai';
2
+ import { convertToModelMessages, streamText, generateId } from 'ai';
3
3
  import { logger } from '../../../logger/index.js';
4
4
  import { createFillingModel } from './model.js';
5
+ import { mode2part } from './utils.js';
6
+ import { batchFillForm } from './batch-fill.controller.js';
5
7
  /**
6
8
  * 表单填写
7
9
  * @param c
@@ -17,12 +19,26 @@ export const fillForm = async (c) => {
17
19
  return c.json({ error: 'Invalid JSON' }, 400);
18
20
  }
19
21
  const formStructure = requestBody.formStructure;
22
+ const mode = requestBody.mode;
23
+ const num = requestBody.count;
24
+ const messages = requestBody.messages;
25
+ // 如果是批量填写,调用批量填写接口
26
+ if (mode === 'batch') {
27
+ return batchFillForm(c);
28
+ }
20
29
  if (formStructure) {
30
+ const extraPart = mode2part(mode, num);
21
31
  try {
22
- let lastMsg = requestBody.messages[requestBody.messages.length - 1];
23
- lastMsg.parts.push({
24
- type: 'text',
25
- text: JSON.stringify(formStructure),
32
+ messages.push({
33
+ id: generateId(),
34
+ role: 'user',
35
+ parts: [
36
+ {
37
+ type: 'text',
38
+ text: `formStructure: ${JSON.stringify(formStructure)}`,
39
+ },
40
+ extraPart,
41
+ ],
26
42
  });
27
43
  }
28
44
  catch (e) {
@@ -31,11 +47,17 @@ export const fillForm = async (c) => {
31
47
  }
32
48
  const stream = streamText({
33
49
  model: createFillingModel(),
34
- messages: convertToModelMessages(requestBody.messages),
50
+ messages: convertToModelMessages(messages),
35
51
  includeRawChunks: true,
36
52
  headers: {
37
53
  'x-user-stage': requestBody.stage,
54
+ 'x-user-var': JSON.stringify({
55
+ mode: requestBody.mode,
56
+ }),
57
+ 'x-user-token': c.req.header('authorization') || '',
38
58
  },
39
59
  });
40
- return stream.toUIMessageStreamResponse();
60
+ return stream.toUIMessageStreamResponse({
61
+ originalMessages: messages, // 建议添加,便于消息 ID 管理
62
+ });
41
63
  };
@@ -1,12 +1,23 @@
1
1
  import { createCoze } from '@baishuyun/coze-provider';
2
2
  import { createFieldsFillingResultTransformer } from './createFieldsFillingResultTransformStream.js';
3
3
  import config from 'config';
4
+ import { createBatchFillingResultTransformer } from './createBatchFillingTransformStream.js';
4
5
  export const createFillingModel = (enableParser = true) => {
5
6
  const aiFormFilling = createCoze({
6
7
  apiKey: config.get('agent.form.fill.apiKey'),
7
8
  baseURL: config.get('agent.form.fill.baseUrl'),
8
9
  botId: config.get('agent.form.fill.botId'),
9
- extraStreamTransformers: [createFieldsFillingResultTransformer(enableParser)],
10
+ extraStreamTransformers: [() => createFieldsFillingResultTransformer(enableParser)],
11
+ });
12
+ const formFillingModel = aiFormFilling.chat('chat');
13
+ return formFillingModel;
14
+ };
15
+ export const createBatchFillingModel = (enableParser = true) => {
16
+ const aiFormFilling = createCoze({
17
+ apiKey: config.get('agent.form.fill.apiKey'),
18
+ baseURL: config.get('agent.form.fill.baseUrl'),
19
+ botId: config.get('agent.form.fill.botId'),
20
+ extraStreamTransformers: [() => createBatchFillingResultTransformer(enableParser)],
10
21
  });
11
22
  const formFillingModel = aiFormFilling.chat('chat');
12
23
  return formFillingModel;