persona-core 1.0.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.
Files changed (218) hide show
  1. package/dist/db/client.d.ts +55 -0
  2. package/dist/db/client.d.ts.map +1 -0
  3. package/dist/db/client.js +157 -0
  4. package/dist/db/client.js.map +1 -0
  5. package/dist/db/index.d.ts +7 -0
  6. package/dist/db/index.d.ts.map +1 -0
  7. package/dist/db/index.js +7 -0
  8. package/dist/db/index.js.map +1 -0
  9. package/dist/db/repositories/executionRepository.d.ts +15 -0
  10. package/dist/db/repositories/executionRepository.d.ts.map +1 -0
  11. package/dist/db/repositories/executionRepository.js +41 -0
  12. package/dist/db/repositories/executionRepository.js.map +1 -0
  13. package/dist/db/repositories/index.d.ts +10 -0
  14. package/dist/db/repositories/index.d.ts.map +1 -0
  15. package/dist/db/repositories/index.js +10 -0
  16. package/dist/db/repositories/index.js.map +1 -0
  17. package/dist/db/repositories/nodeRepository.d.ts +15 -0
  18. package/dist/db/repositories/nodeRepository.d.ts.map +1 -0
  19. package/dist/db/repositories/nodeRepository.js +61 -0
  20. package/dist/db/repositories/nodeRepository.js.map +1 -0
  21. package/dist/db/repositories/personaRepository.d.ts +13 -0
  22. package/dist/db/repositories/personaRepository.d.ts.map +1 -0
  23. package/dist/db/repositories/personaRepository.js +42 -0
  24. package/dist/db/repositories/personaRepository.js.map +1 -0
  25. package/dist/db/repositories/planResultRepository.d.ts +13 -0
  26. package/dist/db/repositories/planResultRepository.d.ts.map +1 -0
  27. package/dist/db/repositories/planResultRepository.js +30 -0
  28. package/dist/db/repositories/planResultRepository.js.map +1 -0
  29. package/dist/db/repositories/scheduleRepository.d.ts +27 -0
  30. package/dist/db/repositories/scheduleRepository.d.ts.map +1 -0
  31. package/dist/db/repositories/scheduleRepository.js +187 -0
  32. package/dist/db/repositories/scheduleRepository.js.map +1 -0
  33. package/dist/db/repositories/types.d.ts +132 -0
  34. package/dist/db/repositories/types.d.ts.map +1 -0
  35. package/dist/db/repositories/types.js +5 -0
  36. package/dist/db/repositories/types.js.map +1 -0
  37. package/dist/db/schema.d.ts +742 -0
  38. package/dist/db/schema.d.ts.map +1 -0
  39. package/dist/db/schema.js +85 -0
  40. package/dist/db/schema.js.map +1 -0
  41. package/dist/index.d.ts +11 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +16 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/llm/capabilities.d.ts +56 -0
  46. package/dist/llm/capabilities.d.ts.map +1 -0
  47. package/dist/llm/capabilities.js +305 -0
  48. package/dist/llm/capabilities.js.map +1 -0
  49. package/dist/llm/index.d.ts +7 -0
  50. package/dist/llm/index.d.ts.map +1 -0
  51. package/dist/llm/index.js +10 -0
  52. package/dist/llm/index.js.map +1 -0
  53. package/dist/llm/interfaces.d.ts +249 -0
  54. package/dist/llm/interfaces.d.ts.map +1 -0
  55. package/dist/llm/interfaces.js +5 -0
  56. package/dist/llm/interfaces.js.map +1 -0
  57. package/dist/llm/providers/anthropic-compatible.d.ts +48 -0
  58. package/dist/llm/providers/anthropic-compatible.d.ts.map +1 -0
  59. package/dist/llm/providers/anthropic-compatible.js +163 -0
  60. package/dist/llm/providers/anthropic-compatible.js.map +1 -0
  61. package/dist/llm/providers/index.d.ts +14 -0
  62. package/dist/llm/providers/index.d.ts.map +1 -0
  63. package/dist/llm/providers/index.js +12 -0
  64. package/dist/llm/providers/index.js.map +1 -0
  65. package/dist/llm/providers/openai-compatible.d.ts +59 -0
  66. package/dist/llm/providers/openai-compatible.d.ts.map +1 -0
  67. package/dist/llm/providers/openai-compatible.js +207 -0
  68. package/dist/llm/providers/openai-compatible.js.map +1 -0
  69. package/dist/services/actionService.d.ts +132 -0
  70. package/dist/services/actionService.d.ts.map +1 -0
  71. package/dist/services/actionService.js +971 -0
  72. package/dist/services/actionService.js.map +1 -0
  73. package/dist/services/branchService.d.ts +19 -0
  74. package/dist/services/branchService.d.ts.map +1 -0
  75. package/dist/services/branchService.js +50 -0
  76. package/dist/services/branchService.js.map +1 -0
  77. package/dist/services/expressionEvaluator.d.ts +16 -0
  78. package/dist/services/expressionEvaluator.d.ts.map +1 -0
  79. package/dist/services/expressionEvaluator.js +70 -0
  80. package/dist/services/expressionEvaluator.js.map +1 -0
  81. package/dist/services/factory.d.ts +43 -0
  82. package/dist/services/factory.d.ts.map +1 -0
  83. package/dist/services/factory.js +30 -0
  84. package/dist/services/factory.js.map +1 -0
  85. package/dist/services/index.d.ts +15 -0
  86. package/dist/services/index.d.ts.map +1 -0
  87. package/dist/services/index.js +17 -0
  88. package/dist/services/index.js.map +1 -0
  89. package/dist/services/interfaces.d.ts +117 -0
  90. package/dist/services/interfaces.d.ts.map +1 -0
  91. package/dist/services/interfaces.js +5 -0
  92. package/dist/services/interfaces.js.map +1 -0
  93. package/dist/services/loaders/fileLoader.d.ts +56 -0
  94. package/dist/services/loaders/fileLoader.d.ts.map +1 -0
  95. package/dist/services/loaders/fileLoader.js +161 -0
  96. package/dist/services/loaders/fileLoader.js.map +1 -0
  97. package/dist/services/loaders/index.d.ts +6 -0
  98. package/dist/services/loaders/index.d.ts.map +1 -0
  99. package/dist/services/loaders/index.js +6 -0
  100. package/dist/services/loaders/index.js.map +1 -0
  101. package/dist/services/loaders/transformers.d.ts +32 -0
  102. package/dist/services/loaders/transformers.d.ts.map +1 -0
  103. package/dist/services/loaders/transformers.js +78 -0
  104. package/dist/services/loaders/transformers.js.map +1 -0
  105. package/dist/services/mockAction.d.ts +65 -0
  106. package/dist/services/mockAction.d.ts.map +1 -0
  107. package/dist/services/mockAction.js +153 -0
  108. package/dist/services/mockAction.js.map +1 -0
  109. package/dist/services/mockBranch.d.ts +50 -0
  110. package/dist/services/mockBranch.d.ts.map +1 -0
  111. package/dist/services/mockBranch.js +75 -0
  112. package/dist/services/mockBranch.js.map +1 -0
  113. package/dist/services/mockThinking.d.ts +68 -0
  114. package/dist/services/mockThinking.d.ts.map +1 -0
  115. package/dist/services/mockThinking.js +89 -0
  116. package/dist/services/mockThinking.js.map +1 -0
  117. package/dist/services/thinkingService.d.ts +15 -0
  118. package/dist/services/thinkingService.d.ts.map +1 -0
  119. package/dist/services/thinkingService.js +117 -0
  120. package/dist/services/thinkingService.js.map +1 -0
  121. package/dist/temporal/activities/actionActivities.d.ts +15 -0
  122. package/dist/temporal/activities/actionActivities.d.ts.map +1 -0
  123. package/dist/temporal/activities/actionActivities.js +140 -0
  124. package/dist/temporal/activities/actionActivities.js.map +1 -0
  125. package/dist/temporal/activities/branchActivities.d.ts +13 -0
  126. package/dist/temporal/activities/branchActivities.d.ts.map +1 -0
  127. package/dist/temporal/activities/branchActivities.js +26 -0
  128. package/dist/temporal/activities/branchActivities.js.map +1 -0
  129. package/dist/temporal/activities/dbActivities.d.ts +14 -0
  130. package/dist/temporal/activities/dbActivities.d.ts.map +1 -0
  131. package/dist/temporal/activities/dbActivities.js +84 -0
  132. package/dist/temporal/activities/dbActivities.js.map +1 -0
  133. package/dist/temporal/activities/index.d.ts +9 -0
  134. package/dist/temporal/activities/index.d.ts.map +1 -0
  135. package/dist/temporal/activities/index.js +9 -0
  136. package/dist/temporal/activities/index.js.map +1 -0
  137. package/dist/temporal/activities/thinkingActivities.d.ts +17 -0
  138. package/dist/temporal/activities/thinkingActivities.d.ts.map +1 -0
  139. package/dist/temporal/activities/thinkingActivities.js +145 -0
  140. package/dist/temporal/activities/thinkingActivities.js.map +1 -0
  141. package/dist/temporal/activities/types.d.ts +100 -0
  142. package/dist/temporal/activities/types.d.ts.map +1 -0
  143. package/dist/temporal/activities/types.js +5 -0
  144. package/dist/temporal/activities/types.js.map +1 -0
  145. package/dist/temporal/client.d.ts +43 -0
  146. package/dist/temporal/client.d.ts.map +1 -0
  147. package/dist/temporal/client.js +75 -0
  148. package/dist/temporal/client.js.map +1 -0
  149. package/dist/temporal/index.d.ts +10 -0
  150. package/dist/temporal/index.d.ts.map +1 -0
  151. package/dist/temporal/index.js +12 -0
  152. package/dist/temporal/index.js.map +1 -0
  153. package/dist/temporal/personaCoreClient.d.ts +199 -0
  154. package/dist/temporal/personaCoreClient.d.ts.map +1 -0
  155. package/dist/temporal/personaCoreClient.js +233 -0
  156. package/dist/temporal/personaCoreClient.js.map +1 -0
  157. package/dist/temporal/personaWorker.d.ts +141 -0
  158. package/dist/temporal/personaWorker.d.ts.map +1 -0
  159. package/dist/temporal/personaWorker.js +93 -0
  160. package/dist/temporal/personaWorker.js.map +1 -0
  161. package/dist/temporal/worker.d.ts +66 -0
  162. package/dist/temporal/worker.d.ts.map +1 -0
  163. package/dist/temporal/worker.js +109 -0
  164. package/dist/temporal/worker.js.map +1 -0
  165. package/dist/temporal/workflows/index.d.ts +5 -0
  166. package/dist/temporal/workflows/index.d.ts.map +1 -0
  167. package/dist/temporal/workflows/index.js +5 -0
  168. package/dist/temporal/workflows/index.js.map +1 -0
  169. package/dist/temporal/workflows/scheduleWorkflow.d.ts +31 -0
  170. package/dist/temporal/workflows/scheduleWorkflow.d.ts.map +1 -0
  171. package/dist/temporal/workflows/scheduleWorkflow.js +256 -0
  172. package/dist/temporal/workflows/scheduleWorkflow.js.map +1 -0
  173. package/dist/types/common.d.ts +81 -0
  174. package/dist/types/common.d.ts.map +1 -0
  175. package/dist/types/common.js +5 -0
  176. package/dist/types/common.js.map +1 -0
  177. package/dist/types/index.d.ts +8 -0
  178. package/dist/types/index.d.ts.map +1 -0
  179. package/dist/types/index.js +12 -0
  180. package/dist/types/index.js.map +1 -0
  181. package/dist/types/nodes.d.ts +496 -0
  182. package/dist/types/nodes.d.ts.map +1 -0
  183. package/dist/types/nodes.js +5 -0
  184. package/dist/types/nodes.js.map +1 -0
  185. package/dist/types/persona.d.ts +59 -0
  186. package/dist/types/persona.d.ts.map +1 -0
  187. package/dist/types/persona.js +36 -0
  188. package/dist/types/persona.js.map +1 -0
  189. package/dist/types/schedule.d.ts +143 -0
  190. package/dist/types/schedule.d.ts.map +1 -0
  191. package/dist/types/schedule.js +155 -0
  192. package/dist/types/schedule.js.map +1 -0
  193. package/dist/utils/dateUtils.d.ts +31 -0
  194. package/dist/utils/dateUtils.d.ts.map +1 -0
  195. package/dist/utils/dateUtils.js +53 -0
  196. package/dist/utils/dateUtils.js.map +1 -0
  197. package/dist/utils/index.d.ts +8 -0
  198. package/dist/utils/index.d.ts.map +1 -0
  199. package/dist/utils/index.js +8 -0
  200. package/dist/utils/index.js.map +1 -0
  201. package/dist/utils/inputResolver.d.ts +43 -0
  202. package/dist/utils/inputResolver.d.ts.map +1 -0
  203. package/dist/utils/inputResolver.js +137 -0
  204. package/dist/utils/inputResolver.js.map +1 -0
  205. package/dist/utils/sharedDataUtils.d.ts +36 -0
  206. package/dist/utils/sharedDataUtils.d.ts.map +1 -0
  207. package/dist/utils/sharedDataUtils.js +84 -0
  208. package/dist/utils/sharedDataUtils.js.map +1 -0
  209. package/dist/utils/typeGuards.d.ts +33 -0
  210. package/dist/utils/typeGuards.d.ts.map +1 -0
  211. package/dist/utils/typeGuards.js +50 -0
  212. package/dist/utils/typeGuards.js.map +1 -0
  213. package/docs/add-llm-provider.md +353 -0
  214. package/docs/external/deepseek-v32.md +28 -0
  215. package/docs/quick-start.md +849 -0
  216. package/docs/suite-guide.md +148 -0
  217. package/docs/usage-guide.md +1487 -0
  218. package/package.json +80 -0
@@ -0,0 +1,1487 @@
1
+ # PersonaCore 使用指南
2
+
3
+ PersonaCore 是一个基于 Temporal 的动态规划 Workflow Agent 工具,支持运行时动态生成和修改工作流节点。
4
+
5
+ ## 目录
6
+
7
+ 1. [核心概念](#核心概念)
8
+ 2. [快速开始](#快速开始)
9
+ 3. [类型定义详解](#类型定义详解)
10
+ 4. [创建 Persona](#创建-persona)
11
+ 5. [创建 Schedule](#创建-schedule)
12
+ 6. [执行 Schedule](#执行-schedule)
13
+ 7. [实现服务接口](#实现服务接口)
14
+ 8. [数据库配置](#数据库配置)
15
+ 9. [测试指南](#测试指南)
16
+ 10. [高级用法](#高级用法)
17
+
18
+ ---
19
+
20
+ ## 核心概念
21
+
22
+ ### Persona(人格)
23
+
24
+ Persona 定义了 Agent 的角色和能力:
25
+ - **System Prompt**:定义角色的行为风格和基本指令
26
+ - **Action Types**:该角色可以执行的动作类型列表
27
+
28
+ 思考节点(Thinking)和分支节点(Branch)是系统内置的,不需要在 Persona 中定义。
29
+
30
+ ### Schedule(执行计划)
31
+
32
+ Schedule 是一次确定的执行计划,由节点实例链表组成:
33
+ - **入口节点**:第一个节点必须是思考节点
34
+ - **共享数据**:Schedule 级别的数据存储,节点间可以通过它共享数据
35
+ - **动态扩展**:思考节点可以在运行时生成新的节点
36
+
37
+ ### 节点类型
38
+
39
+ | 类型 | 说明 | 特点 |
40
+ |------|------|------|
41
+ | **Thinking** | 思考节点 | 负责规划后续节点,可以声明共享数据 |
42
+ | **Action** | 动作节点 | 执行具体操作(API 调用、数据库查询等) |
43
+ | **Branch** | 分支节点 | 根据条件选择不同的执行路径 |
44
+
45
+ ### 共享数据
46
+
47
+ 共享数据分为两种类型:
48
+ - **普通类型**:赋值会覆盖之前的值
49
+ - **集合类型**:赋值会追加到数组中
50
+
51
+ ---
52
+
53
+ ## 快速开始
54
+
55
+ ### 安装依赖
56
+
57
+ ```bash
58
+ npm install persona-core
59
+ # 或
60
+ pnpm add persona-core
61
+ ```
62
+
63
+ ### 前置条件
64
+
65
+ 1. 运行中的 Temporal Server(本地开发可使用 Docker)
66
+ 2. PostgreSQL 数据库(测试可使用 PGlite)
67
+
68
+ ### 最小示例
69
+
70
+ ```typescript
71
+ import {
72
+ Persona,
73
+ Schedule,
74
+ ThinkingNodeInstance,
75
+ ActionNodeInstance,
76
+ ScheduleClient,
77
+ createWorker,
78
+ createDatabaseClient,
79
+ runMigrations,
80
+ } from 'persona-core';
81
+ import { PGlite } from '@electric-sql/pglite';
82
+
83
+ // 1. 创建 Persona
84
+ const persona: Persona = {
85
+ id: 'my-persona',
86
+ name: '示例 Agent',
87
+ systemPrompt: '你是一个有帮助的助手',
88
+ actionTypes: [
89
+ {
90
+ kind: 'action',
91
+ id: 'greet',
92
+ name: '打招呼',
93
+ executionType: 'llm_call',
94
+ inputFields: [{ name: 'name', required: true, typeHint: 'string' }],
95
+ outputFields: [{ name: 'greeting', required: true, typeHint: 'string' }],
96
+ },
97
+ ],
98
+ createdAt: new Date(),
99
+ updatedAt: new Date(),
100
+ };
101
+
102
+ // 2. 创建 Schedule
103
+ const thinkingNode: ThinkingNodeInstance = {
104
+ id: 'think-1',
105
+ kind: 'thinking',
106
+ thinkingPrompt: '规划一个问候流程',
107
+ inputs: {},
108
+ nextNodeId: 'action-1',
109
+ };
110
+
111
+ const actionNode: ActionNodeInstance = {
112
+ id: 'action-1',
113
+ kind: 'action',
114
+ actionTypeId: 'greet',
115
+ inputs: {
116
+ name: { type: 'fixed', value: 'World' },
117
+ },
118
+ outputMappings: [],
119
+ nextNodeId: null,
120
+ };
121
+
122
+ const schedule: Schedule = {
123
+ id: 'schedule-1',
124
+ personaId: persona.id,
125
+ name: '示例 Schedule',
126
+ entryNodeId: thinkingNode.id,
127
+ nodes: {
128
+ [thinkingNode.id]: thinkingNode,
129
+ [actionNode.id]: actionNode,
130
+ },
131
+ sharedData: { data: {}, fieldTypes: {} },
132
+ status: 'pending',
133
+ executionHistory: [],
134
+ createdAt: new Date(),
135
+ updatedAt: new Date(),
136
+ };
137
+
138
+ // 3. 初始化数据库
139
+ const pglite = new PGlite();
140
+ await runMigrations(pglite);
141
+ const db = createDatabaseClient(pglite);
142
+
143
+ // 4. 启动 Worker 和 Client(需要 Temporal Server 运行中)
144
+ // 详见后续章节
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 类型定义详解
150
+
151
+ ### 节点输入值
152
+
153
+ 节点输入支持两种方式:
154
+
155
+ ```typescript
156
+ // 固定值
157
+ const fixedInput: FixedValue = {
158
+ type: 'fixed',
159
+ value: '固定的字符串值',
160
+ };
161
+
162
+ // 引用共享数据
163
+ const referenceInput: DataReference = {
164
+ type: 'reference',
165
+ fieldName: 'topics', // 引用的共享数据字段名
166
+ path: '[0].name', // 可选:JSONPath 表达式
167
+ };
168
+ ```
169
+
170
+ ### 输出映射
171
+
172
+ 定义如何将节点输出写入共享数据:
173
+
174
+ ```typescript
175
+ const outputMapping: OutputMapping = {
176
+ outputField: 'postId', // 节点输出的字段名
177
+ targetField: 'createdPost', // 目标共享数据字段名
178
+ expression: '"post:" + postId', // 可选:转换表达式
179
+ };
180
+ ```
181
+
182
+ ### 重试配置
183
+
184
+ 动作节点支持自动重试:
185
+
186
+ ```typescript
187
+ const retryConfig: RetryConfig = {
188
+ maxRetries: 3,
189
+ retryIntervalMs: 1000,
190
+ exponentialBackoff: true, // 使用指数退避
191
+ };
192
+ ```
193
+
194
+ ### 异步动作配置
195
+
196
+ 支持需要轮询的异步操作:
197
+
198
+ ```typescript
199
+ const asyncConfig: AsyncModeConfig = {
200
+ isAsync: true,
201
+ defaultCheckIntervalMs: 5000, // 轮询间隔
202
+ defaultTimeoutMs: 300000, // 超时时间
203
+ };
204
+ ```
205
+
206
+ ---
207
+
208
+ ## 创建 Persona
209
+
210
+ ### 定义动作类型
211
+
212
+ ```typescript
213
+ import { ActionNodeType, Persona } from 'persona-core';
214
+
215
+ const searchAction: ActionNodeType = {
216
+ kind: 'action',
217
+ id: 'search_topics',
218
+ name: '搜索热门话题',
219
+ description: '搜索当前热门的话题和趋势',
220
+ executionType: 'api_call', // 执行类型
221
+ inputFields: [
222
+ { name: 'keywords', required: false, typeHint: 'string' },
223
+ { name: 'limit', required: false, typeHint: 'number' },
224
+ ],
225
+ outputFields: [
226
+ { name: 'topics', required: true, typeHint: 'array' },
227
+ ],
228
+ };
229
+
230
+ const createPostAction: ActionNodeType = {
231
+ kind: 'action',
232
+ id: 'create_post',
233
+ name: '创建帖子',
234
+ executionType: 'api_call',
235
+ // 异步模式:需要提交任务后轮询结果
236
+ asyncMode: {
237
+ isAsync: true,
238
+ defaultCheckIntervalMs: 3000,
239
+ defaultTimeoutMs: 60000,
240
+ },
241
+ inputFields: [
242
+ { name: 'content', required: true, typeHint: 'string' },
243
+ ],
244
+ outputFields: [
245
+ { name: 'postId', required: true, typeHint: 'string' },
246
+ { name: 'url', required: true, typeHint: 'string' },
247
+ ],
248
+ };
249
+ ```
250
+
251
+ ### 执行类型
252
+
253
+ | 类型 | 说明 |
254
+ |------|------|
255
+ | `api_call` | API 调用 |
256
+ | `llm_call` | LLM 调用 |
257
+ | `database_query` | 数据库查询 |
258
+ | `file_write` | 文件写入 |
259
+ | `file_read` | 文件读取 |
260
+ | `http_request` | HTTP 请求 |
261
+ | `shell_command` | Shell 命令 |
262
+
263
+ ### API 调用配置 (apiConfig)
264
+
265
+ 当 `executionType` 为 `api_call` 时,可以通过 `apiConfig` 配置 HTTP 请求的详细参数:
266
+
267
+ ```typescript
268
+ const apiAction: ActionNodeType = {
269
+ kind: 'action',
270
+ id: 'call_external_api',
271
+ name: '调用外部 API',
272
+ executionType: 'api_call',
273
+ apiConfig: {
274
+ url: 'https://api.example.com/data/${userId}', // 支持 ${变量} 模板
275
+ method: 'POST',
276
+ headers: {
277
+ type: 'static', // 'static' | 'template'
278
+ values: {
279
+ 'Accept': 'application/json',
280
+ 'Authorization': 'Bearer your-token',
281
+ },
282
+ },
283
+ body: {
284
+ contentType: 'json', // 'json' | 'form' | 'raw' | 'template'
285
+ includeFields: ['field1', 'field2'], // 可选:仅包含指定字段
286
+ },
287
+ response: {
288
+ type: 'json',
289
+ extract: {
290
+ result: 'data.result', // JSONPath 提取
291
+ total: 'meta.total',
292
+ },
293
+ },
294
+ timeoutMs: 30000,
295
+ },
296
+ inputFields: [
297
+ { name: 'userId', required: true, typeHint: 'string' },
298
+ { name: 'field1', required: true, typeHint: 'string' },
299
+ ],
300
+ outputFields: [
301
+ { name: 'result', required: true, typeHint: 'object' },
302
+ { name: 'total', required: true, typeHint: 'number' },
303
+ ],
304
+ };
305
+ ```
306
+
307
+ #### Headers 配置
308
+
309
+ Headers 支持两种模式,与 body 配置保持统一设计:
310
+
311
+ **静态模式:**
312
+ ```typescript
313
+ headers: {
314
+ type: 'static',
315
+ values: {
316
+ 'Accept': 'application/json',
317
+ 'Authorization': 'Bearer fixed-token',
318
+ },
319
+ }
320
+ ```
321
+
322
+ **模板模式(支持 Nunjucks 语法):**
323
+ ```typescript
324
+ headers: {
325
+ type: 'template',
326
+ base: { 'Accept': 'application/json' }, // 基础 headers(优先级最低)
327
+ template: `{
328
+ "Authorization": "Bearer {{ apiKey }}",
329
+ "X-Request-ID": "{{ requestId }}"
330
+ {% if customHeader %}, "X-Custom": "{{ customHeader }}"{% endif %}
331
+ }`,
332
+ }
333
+ ```
334
+
335
+ | 字段 | 类型 | 说明 |
336
+ |------|------|------|
337
+ | `type` | `'static' \| 'template'` | 配置模式 |
338
+ | `values` | `Record<string, string>` | 静态模式下的请求头键值对 |
339
+ | `template` | `string` | 模板模式下的 Nunjucks 模板,需返回 JSON 对象 |
340
+ | `base` | `Record<string, string>` | 基础请求头,会与 values/template 结果合并(优先级最低) |
341
+
342
+ #### Body 配置
343
+
344
+ | contentType | 说明 |
345
+ |-------------|------|
346
+ | `json` | 将 inputFields 序列化为 JSON |
347
+ | `form` | 将 inputFields 序列化为 form-urlencoded |
348
+ | `raw` | 使用指定字段的原始值 |
349
+ | `template` | 使用 Nunjucks 模板构建请求体 |
350
+
351
+ **模板模式示例:**
352
+ ```typescript
353
+ body: {
354
+ contentType: 'template',
355
+ template: `{
356
+ "model": "gpt-4",
357
+ "messages": [
358
+ { "role": "user", "content": {{ prompt | dump }} }
359
+ ]
360
+ }`,
361
+ }
362
+ ```
363
+
364
+ ### 创建完整 Persona
365
+
366
+ ```typescript
367
+ const persona: Persona = {
368
+ id: 'social-creator',
369
+ name: '社交网络内容创作者',
370
+ description: '擅长创作吸引眼球的内容',
371
+ systemPrompt: `你是一个社交网络内容创作者,非常幽默风趣。
372
+ 你的任务是:
373
+ 1. 关注热门话题和趋势
374
+ 2. 创作有趣、有价值的内容
375
+ 3. 与粉丝积极互动`,
376
+ actionTypes: [searchAction, createPostAction],
377
+ createdAt: new Date(),
378
+ updatedAt: new Date(),
379
+ };
380
+ ```
381
+
382
+ ---
383
+
384
+ ## 创建 Schedule
385
+
386
+ ### 思考节点
387
+
388
+ 思考节点负责规划后续流程:
389
+
390
+ ```typescript
391
+ import { ThinkingNodeInstance } from 'persona-core';
392
+
393
+ const thinkingNode: ThinkingNodeInstance = {
394
+ id: 'think-1',
395
+ kind: 'thinking',
396
+ thinkingPrompt: '分析当前热门话题,规划一个内容创作计划',
397
+ inputs: {
398
+ // 可以读取共享数据来辅助决策
399
+ existingTopics: { type: 'reference', fieldName: 'topics' },
400
+ },
401
+ // 声明共享数据字段
402
+ sharedDataDeclaration: {
403
+ fields: [
404
+ { name: 'topics', isCollection: true, description: '搜索到的话题' },
405
+ { name: 'createdPostId', isCollection: false },
406
+ ],
407
+ },
408
+ // 可选:将分析结果写入共享数据
409
+ outputMappings: [
410
+ { outputField: 'analysis', targetField: 'planAnalysis' },
411
+ ],
412
+ nextNodeId: 'action-1',
413
+ };
414
+ ```
415
+
416
+ ### 动作节点
417
+
418
+ 动作节点执行具体操作:
419
+
420
+ ```typescript
421
+ import { ActionNodeInstance } from 'persona-core';
422
+
423
+ const searchNode: ActionNodeInstance = {
424
+ id: 'search-1',
425
+ kind: 'action',
426
+ actionTypeId: 'search_topics', // 引用 Persona 中定义的动作类型
427
+ inputs: {
428
+ keywords: { type: 'fixed', value: '科技' },
429
+ limit: { type: 'fixed', value: 10 },
430
+ },
431
+ outputMappings: [
432
+ { outputField: 'topics', targetField: 'topics' },
433
+ ],
434
+ retryConfig: {
435
+ maxRetries: 3,
436
+ retryIntervalMs: 1000,
437
+ exponentialBackoff: true,
438
+ },
439
+ nextNodeId: 'create-1',
440
+ };
441
+
442
+ const createNode: ActionNodeInstance = {
443
+ id: 'create-1',
444
+ kind: 'action',
445
+ actionTypeId: 'create_post',
446
+ inputs: {
447
+ content: { type: 'fixed', value: '测试帖子内容' },
448
+ // 引用共享数据
449
+ tags: { type: 'reference', fieldName: 'topics', path: '[0].name' },
450
+ },
451
+ outputMappings: [
452
+ { outputField: 'postId', targetField: 'createdPostId' },
453
+ ],
454
+ // 覆盖动作类型的默认异步配置
455
+ asyncConfig: {
456
+ timeoutMs: 30000,
457
+ checkIntervalMs: 2000,
458
+ },
459
+ nextNodeId: null, // 结束
460
+ };
461
+ ```
462
+
463
+ ### 分支节点
464
+
465
+ 分支节点支持三种决策类型:
466
+
467
+ #### 表达式决策
468
+
469
+ ```typescript
470
+ import { BranchNodeInstance } from 'persona-core';
471
+
472
+ const branchNode: BranchNodeInstance = {
473
+ id: 'branch-1',
474
+ kind: 'branch',
475
+ branches: [
476
+ { branchId: 'positive', name: '正面', targetNodeId: 'publish-1' },
477
+ { branchId: 'negative', name: '负面', targetNodeId: 'save-draft-1' },
478
+ ],
479
+ inputs: {
480
+ sentiment: { type: 'reference', fieldName: 'sentimentResult' },
481
+ },
482
+ decisionConfig: {
483
+ type: 'expression',
484
+ conditions: {
485
+ 'positive': 'sentiment == "positive"',
486
+ 'negative': 'sentiment == "negative"',
487
+ },
488
+ defaultBranchId: 'negative',
489
+ },
490
+ nextNodeId: null,
491
+ };
492
+ ```
493
+
494
+ #### LLM 决策
495
+
496
+ ```typescript
497
+ const llmBranchNode: BranchNodeInstance = {
498
+ id: 'llm-branch-1',
499
+ kind: 'branch',
500
+ branches: [
501
+ { branchId: 'tech', name: '科技类', targetNodeId: 'tech-handler' },
502
+ { branchId: 'life', name: '生活类', targetNodeId: 'life-handler' },
503
+ ],
504
+ inputs: {
505
+ content: { type: 'reference', fieldName: 'postContent' },
506
+ },
507
+ decisionConfig: {
508
+ type: 'llm',
509
+ prompt: '根据内容判断属于哪个类别:tech(科技)或 life(生活)',
510
+ },
511
+ nextNodeId: null,
512
+ };
513
+ ```
514
+
515
+ #### 函数决策
516
+
517
+ ```typescript
518
+ const funcBranchNode: BranchNodeInstance = {
519
+ id: 'func-branch-1',
520
+ kind: 'branch',
521
+ branches: [
522
+ { branchId: 'morning', name: '早间', targetNodeId: 'morning-handler' },
523
+ { branchId: 'evening', name: '晚间', targetNodeId: 'evening-handler' },
524
+ ],
525
+ inputs: {
526
+ currentTime: { type: 'fixed', value: new Date().toISOString() },
527
+ },
528
+ decisionConfig: {
529
+ type: 'function',
530
+ functionName: 'determineTimeSlot', // 需要在运行时注册
531
+ },
532
+ nextNodeId: null,
533
+ };
534
+ ```
535
+
536
+ ### 构建完整 Schedule
537
+
538
+ ```typescript
539
+ import { Schedule } from 'persona-core';
540
+
541
+ const schedule: Schedule = {
542
+ id: 'my-schedule-1',
543
+ personaId: 'social-creator',
544
+ name: '内容创作流程',
545
+ description: '搜索话题并创建帖子',
546
+ entryNodeId: 'think-1',
547
+ nodes: {
548
+ 'think-1': thinkingNode,
549
+ 'search-1': searchNode,
550
+ 'branch-1': branchNode,
551
+ 'create-1': createNode,
552
+ // ... 其他节点
553
+ },
554
+ sharedData: {
555
+ data: {
556
+ // 初始数据
557
+ targetAudience: 'developers',
558
+ },
559
+ fieldTypes: {
560
+ topics: true, // 集合类型
561
+ targetAudience: false, // 普通类型
562
+ createdPostId: false,
563
+ },
564
+ },
565
+ status: 'pending',
566
+ executionHistory: [],
567
+ createdAt: new Date(),
568
+ updatedAt: new Date(),
569
+ };
570
+ ```
571
+
572
+ ### 循环结构
573
+
574
+ 支持节点循环,但循环中必须包含至少一个可以退出的分支节点:
575
+
576
+ ```typescript
577
+ const loopSchedule: Schedule = {
578
+ id: 'loop-schedule',
579
+ personaId: 'social-creator',
580
+ name: '循环收集话题',
581
+ entryNodeId: 'think-1',
582
+ nodes: {
583
+ 'think-1': {
584
+ id: 'think-1',
585
+ kind: 'thinking',
586
+ thinkingPrompt: '持续搜索直到找到足够的素材',
587
+ inputs: {},
588
+ sharedDataDeclaration: {
589
+ fields: [
590
+ { name: 'topics', isCollection: true },
591
+ { name: 'targetCount', isCollection: false, initialValue: 5 },
592
+ ],
593
+ },
594
+ nextNodeId: 'search-1',
595
+ },
596
+ 'search-1': {
597
+ id: 'search-1',
598
+ kind: 'action',
599
+ actionTypeId: 'search_topics',
600
+ inputs: { limit: { type: 'fixed', value: 2 } },
601
+ outputMappings: [{ outputField: 'topics', targetField: 'topics' }],
602
+ nextNodeId: 'check-1',
603
+ },
604
+ 'check-1': {
605
+ id: 'check-1',
606
+ kind: 'branch',
607
+ branches: [
608
+ { branchId: 'enough', name: '足够', targetNodeId: 'create-1' },
609
+ { branchId: 'not_enough', name: '不足', targetNodeId: 'search-1' }, // 循环
610
+ ],
611
+ inputs: {
612
+ collected: { type: 'reference', fieldName: 'topics' },
613
+ target: { type: 'reference', fieldName: 'targetCount' },
614
+ },
615
+ decisionConfig: {
616
+ type: 'expression',
617
+ conditions: {
618
+ 'enough': 'collected.length >= target',
619
+ 'not_enough': 'collected.length < target',
620
+ },
621
+ defaultBranchId: 'not_enough',
622
+ },
623
+ nextNodeId: null,
624
+ },
625
+ 'create-1': {
626
+ id: 'create-1',
627
+ kind: 'action',
628
+ actionTypeId: 'create_post',
629
+ inputs: { content: { type: 'fixed', value: '综合帖子' } },
630
+ outputMappings: [],
631
+ nextNodeId: null,
632
+ },
633
+ },
634
+ sharedData: { data: { topics: [] }, fieldTypes: { topics: true } },
635
+ status: 'pending',
636
+ executionHistory: [],
637
+ createdAt: new Date(),
638
+ updatedAt: new Date(),
639
+ };
640
+ ```
641
+
642
+ ---
643
+
644
+ ## 执行 Schedule
645
+
646
+ ### 配置 Temporal Worker
647
+
648
+ ```typescript
649
+ import {
650
+ createWorker,
651
+ WorkerConfig,
652
+ DatabaseClient,
653
+ ThinkingService,
654
+ ActionService,
655
+ BranchService,
656
+ } from 'persona-core';
657
+
658
+ // 实现服务接口(见下一章节)
659
+ const thinkingService: ThinkingService = /* ... */;
660
+ const actionService: ActionService = /* ... */;
661
+ const branchService: BranchService = /* ... */;
662
+
663
+ const workerConfig: WorkerConfig = {
664
+ temporalAddress: 'localhost:7233',
665
+ taskQueue: 'persona-core-queue',
666
+ db: databaseClient,
667
+ thinkingService,
668
+ actionService,
669
+ branchService,
670
+ connectTimeout: 10000, // 可选:连接超时(毫秒)
671
+ };
672
+
673
+ // 创建 Worker(返回 WorkerHandle)
674
+ const workerHandle = await createWorker(workerConfig);
675
+
676
+ // WorkerHandle 接口:
677
+ // - worker: Worker Temporal Worker 实例
678
+ // - shutdown: () => Promise<void> 优雅关闭方法
679
+
680
+ // 运行 Worker(阻塞)
681
+ await workerHandle.worker.run();
682
+
683
+ // 优雅关闭(等待正在执行的 Activity 完成后关闭)
684
+ await workerHandle.shutdown();
685
+ ```
686
+
687
+ ### 使用 Schedule Client
688
+
689
+ ```typescript
690
+ import { ScheduleClient } from 'persona-core';
691
+ import { Client } from '@temporalio/client';
692
+
693
+ // 方式一:直接创建
694
+ const client = await ScheduleClient.create('localhost:7233');
695
+
696
+ // 方式二:从已有 Temporal Client 创建
697
+ const temporalClient = new Client({ /* config */ });
698
+ const scheduleClient = ScheduleClient.fromClient(temporalClient);
699
+
700
+ // 启动 Schedule 执行
701
+ const workflowId = await client.startSchedule(
702
+ 'my-schedule-1', // Schedule ID
703
+ 'persona-core-queue' // Task Queue
704
+ );
705
+
706
+ // 查询执行状态
707
+ const status = await client.getStatus(workflowId);
708
+ console.log('当前节点:', status.currentNodeId);
709
+ console.log('总迭代次数:', status.totalIterations);
710
+ console.log('是否暂停:', status.isPaused);
711
+
712
+ // 暂停执行
713
+ await client.pauseSchedule(workflowId);
714
+
715
+ // 恢复执行
716
+ await client.resumeSchedule(workflowId);
717
+
718
+ // 取消执行
719
+ await client.cancelSchedule(workflowId);
720
+
721
+ // 等待执行完成
722
+ await client.waitForCompletion(workflowId);
723
+ ```
724
+
725
+ ---
726
+
727
+ ## 实现服务接口
728
+
729
+ ### ThinkingService
730
+
731
+ 思考服务负责规划节点:
732
+
733
+ ```typescript
734
+ import {
735
+ ThinkingService,
736
+ ThinkingContext,
737
+ ThinkingResult,
738
+ ThinkingNodeInstance,
739
+ } from 'persona-core';
740
+
741
+ class MyThinkingService implements ThinkingService {
742
+ async execute(
743
+ node: ThinkingNodeInstance,
744
+ context: ThinkingContext
745
+ ): Promise<ThinkingResult> {
746
+ // 使用 LLM 进行规划
747
+ const response = await callLLM({
748
+ systemPrompt: context.persona.systemPrompt,
749
+ userPrompt: node.thinkingPrompt,
750
+ context: {
751
+ sharedData: context.sharedData,
752
+ availableActions: context.availableActionTypes,
753
+ },
754
+ });
755
+
756
+ // 解析 LLM 响应,生成新节点
757
+ const plannedNodes = parseNodesFromResponse(response);
758
+
759
+ return {
760
+ plannedNodes,
761
+ sharedDataUpdates: response.sharedDataUpdates ?? {},
762
+ sharedDataDeclaration: response.sharedDataDeclaration,
763
+ };
764
+ }
765
+ }
766
+ ```
767
+
768
+ ### ActionService
769
+
770
+ 动作服务负责执行具体操作:
771
+
772
+ ```typescript
773
+ import {
774
+ ActionService,
775
+ ActionContext,
776
+ ActionResult,
777
+ TaskStatus,
778
+ ActionNodeType,
779
+ } from 'persona-core';
780
+
781
+ class MyActionService implements ActionService {
782
+ // 同步执行
783
+ async executeSync(
784
+ actionType: ActionNodeType,
785
+ context: ActionContext
786
+ ): Promise<ActionResult> {
787
+ switch (actionType.executionType) {
788
+ case 'api_call':
789
+ return this.handleApiCall(actionType, context);
790
+ case 'llm_call':
791
+ return this.handleLLMCall(actionType, context);
792
+ // ... 其他类型
793
+ }
794
+ }
795
+
796
+ // 提交异步任务
797
+ async submitAsyncTask(
798
+ actionType: ActionNodeType,
799
+ context: ActionContext
800
+ ): Promise<string> {
801
+ // 返回任务 ID
802
+ const taskId = await externalService.submitTask({
803
+ type: actionType.id,
804
+ inputs: context.resolvedInputs,
805
+ });
806
+ return taskId;
807
+ }
808
+
809
+ // 检查任务状态
810
+ async checkTaskStatus(taskId: string): Promise<TaskStatus> {
811
+ const status = await externalService.getTaskStatus(taskId);
812
+ return {
813
+ completed: status.state === 'completed',
814
+ failed: status.state === 'failed',
815
+ error: status.error,
816
+ progress: status.progress,
817
+ };
818
+ }
819
+
820
+ // 获取任务结果
821
+ async getTaskResult(taskId: string): Promise<ActionResult> {
822
+ const result = await externalService.getTaskResult(taskId);
823
+ return { outputs: result };
824
+ }
825
+ }
826
+ ```
827
+
828
+ ### BranchService
829
+
830
+ 分支服务负责决策:
831
+
832
+ ```typescript
833
+ import {
834
+ BranchService,
835
+ BranchResult,
836
+ BranchNodeInstance,
837
+ SharedDataInstance,
838
+ } from 'persona-core';
839
+
840
+ class MyBranchService implements BranchService {
841
+ async evaluateBranch(
842
+ node: BranchNodeInstance,
843
+ inputs: Record<string, unknown>,
844
+ sharedData: SharedDataInstance
845
+ ): Promise<BranchResult> {
846
+ switch (node.decisionConfig.type) {
847
+ case 'expression':
848
+ return this.evaluateExpression(node, inputs);
849
+ case 'llm':
850
+ return this.evaluateLLM(node, inputs);
851
+ case 'function':
852
+ return this.evaluateFunction(node, inputs);
853
+ }
854
+ }
855
+
856
+ private async evaluateLLM(
857
+ node: BranchNodeInstance,
858
+ inputs: Record<string, unknown>
859
+ ): Promise<BranchResult> {
860
+ const config = node.decisionConfig as LLMDecisionConfig;
861
+
862
+ // 调用 LLM 进行决策
863
+ const response = await callLLM({
864
+ prompt: config.prompt,
865
+ context: inputs,
866
+ options: node.branches.map(b => b.branchId),
867
+ });
868
+
869
+ const selectedBranchId = response.selectedOption;
870
+ const branch = node.branches.find(b => b.branchId === selectedBranchId);
871
+
872
+ return {
873
+ selectedBranchId,
874
+ selectedBranchTargetId: branch!.targetNodeId,
875
+ };
876
+ }
877
+ }
878
+ ```
879
+
880
+ ### 内置 ExpressionBranchService
881
+
882
+ 系统提供了 `ExpressionBranchService`,用于评估表达式类型的分支决策。它使用 [expr-eval](https://www.npmjs.com/package/expr-eval) 库进行表达式解析和评估。
883
+
884
+ ```typescript
885
+ import { ExpressionBranchService } from 'persona-core';
886
+
887
+ // 用于表达式类型的分支决策
888
+ const expressionBranchService = new ExpressionBranchService();
889
+
890
+ // 在 Worker 配置中使用
891
+ const workerConfig: WorkerConfig = {
892
+ // ...
893
+ branchService: expressionBranchService,
894
+ };
895
+ ```
896
+
897
+ #### expr-eval 表达式语法
898
+
899
+ 表达式决策使用 expr-eval 库,支持以下语法:
900
+
901
+ | 语法 | 示例 | 说明 |
902
+ |------|------|------|
903
+ | 比较运算符 | `==`, `!=`, `>`, `<`, `>=`, `<=` | 注意:使用 `==` 而非 `===` |
904
+ | 逻辑运算符 | `and`, `or`, `not` | 不支持 `&&` 和 `||` |
905
+ | 属性访问 | `input.property` | 访问对象属性 |
906
+ | 数组访问 | `array[0]`, `array.length` | 支持索引和 length |
907
+ | 算术运算 | `+`, `-`, `*`, `/`, `%` | 基本数学运算 |
908
+ | 字符串连接 | `"Hello " + name` | 使用 `+` 连接 |
909
+
910
+ **示例表达式:**
911
+
912
+ ```typescript
913
+ // 简单比较
914
+ 'status == "completed"'
915
+
916
+ // 数值比较
917
+ 'count >= 10'
918
+
919
+ // 复合条件
920
+ 'items.length > 0 and status == "ready"'
921
+
922
+ // 逻辑非
923
+ 'not isEmpty'
924
+ ```
925
+
926
+ ### 使用内置 Mock 服务(测试用)
927
+
928
+ ```typescript
929
+ import {
930
+ MockThinkingService,
931
+ MockActionService,
932
+ MockBranchService,
933
+ } from 'persona-core';
934
+
935
+ const mockThinking = new MockThinkingService();
936
+ const mockAction = new MockActionService();
937
+ const mockBranch = new MockBranchService();
938
+
939
+ // 配置特定节点的行为
940
+ mockThinking.configure('think-1', {
941
+ plannedNodes: [/* 预定义的节点 */],
942
+ sharedDataUpdates: { status: 'planned' },
943
+ delayMs: 100,
944
+ });
945
+
946
+ // 配置特定动作类型的行为
947
+ mockAction.configureAction('search_topics', {
948
+ outputs: { topics: [{ name: 'AI' }, { name: 'Web3' }] },
949
+ delayMs: 50,
950
+ });
951
+
952
+ // 配置分支选择
953
+ mockBranch.configure('branch-1', {
954
+ selectedBranchId: 'positive',
955
+ });
956
+
957
+ // 重置所有配置
958
+ mockThinking.reset();
959
+ mockAction.reset();
960
+ mockBranch.reset();
961
+ ```
962
+
963
+ ---
964
+
965
+ ## 数据库配置
966
+
967
+ ### 使用 PGlite(测试/开发)
968
+
969
+ ```typescript
970
+ import { PGlite } from '@electric-sql/pglite';
971
+ import { createDatabaseClient, runMigrations } from 'persona-core';
972
+
973
+ // 创建内存数据库
974
+ const pglite = new PGlite();
975
+ await runMigrations(pglite);
976
+ const db = createDatabaseClient(pglite);
977
+ ```
978
+
979
+ ### 数据库 Schema
980
+
981
+ 系统使用以下表:
982
+
983
+ | 表名 | 说明 |
984
+ |------|------|
985
+ | `schedules` | Schedule 基本信息和状态 |
986
+ | `schedule_nodes` | 节点实例 |
987
+ | `node_executions` | 节点执行历史 |
988
+ | `plan_results` | 思考节点结果(用于幂等性) |
989
+ | `personas` | Persona 定义 |
990
+
991
+ ### Repository 使用
992
+
993
+ ```typescript
994
+ import {
995
+ ScheduleRepositoryImpl,
996
+ NodeRepositoryImpl,
997
+ ExecutionRepositoryImpl,
998
+ } from 'persona-core';
999
+
1000
+ const scheduleRepo = new ScheduleRepositoryImpl(db);
1001
+ const nodeRepo = new NodeRepositoryImpl(db);
1002
+ const executionRepo = new ExecutionRepositoryImpl(db);
1003
+
1004
+ // 创建 Schedule(注意:Schedule 必须包含 persona 字段)
1005
+ await scheduleRepo.createSchedule(schedule);
1006
+
1007
+ // 获取 Schedule(包含所有节点)
1008
+ const scheduleWithNodes = await scheduleRepo.getScheduleWithNodes('schedule-id');
1009
+
1010
+ // 获取执行历史
1011
+ const history = await executionRepo.getExecutionHistory('schedule-id');
1012
+ ```
1013
+
1014
+ ---
1015
+
1016
+ ## 测试指南
1017
+
1018
+ ### 集成测试设置
1019
+
1020
+ ```typescript
1021
+ import { describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
1022
+ import { TestWorkflowEnvironment } from '@temporalio/testing';
1023
+ import { Worker } from '@temporalio/worker';
1024
+ import { fileURLToPath } from 'url';
1025
+ import path from 'path';
1026
+
1027
+ // 测试工具使用相对路径导入(tests 目录不是 npm 包的一部分)
1028
+ import {
1029
+ setupTestDatabase,
1030
+ teardownTestDatabase,
1031
+ createTestScheduleInDB,
1032
+ } from '../setup/database.js';
1033
+ import { createTestServices } from '../setup/services.js';
1034
+ import { createActivities } from '../../src/temporal/worker.js';
1035
+
1036
+ // ESM 环境下获取 __dirname
1037
+ const __filename = fileURLToPath(import.meta.url);
1038
+ const __dirname = path.dirname(__filename);
1039
+
1040
+ describe('Workflow 集成测试', () => {
1041
+ let testEnv: TestWorkflowEnvironment;
1042
+ let dbCtx: TestDatabaseContext;
1043
+ let servicesCtx: TestServicesContext;
1044
+ let worker: Worker;
1045
+
1046
+ beforeAll(async () => {
1047
+ // 设置数据库
1048
+ dbCtx = await setupTestDatabase();
1049
+ // Persona 现在嵌入到 Schedule 中,不再需要单独创建
1050
+
1051
+ // 设置服务
1052
+ servicesCtx = createTestServices();
1053
+
1054
+ // 设置 Temporal 测试环境
1055
+ testEnv = await TestWorkflowEnvironment.createLocal();
1056
+
1057
+ // 创建 Worker
1058
+ const activities = createActivities({
1059
+ db: dbCtx.db,
1060
+ thinkingService: servicesCtx.mockThinking,
1061
+ actionService: servicesCtx.mockAction,
1062
+ branchService: servicesCtx.mockBranch,
1063
+ });
1064
+
1065
+ worker = await Worker.create({
1066
+ connection: testEnv.nativeConnection,
1067
+ taskQueue: 'test-queue',
1068
+ // ESM 环境下使用 path.join 而非 require.resolve
1069
+ workflowsPath: path.join(__dirname, '../../src/temporal/workflows'),
1070
+ activities,
1071
+ });
1072
+
1073
+ worker.run(); // 非阻塞
1074
+ });
1075
+
1076
+ afterAll(async () => {
1077
+ worker.shutdown();
1078
+ await testEnv?.teardown();
1079
+ await teardownTestDatabase(dbCtx);
1080
+ });
1081
+
1082
+ beforeEach(() => {
1083
+ servicesCtx.resetAll();
1084
+ });
1085
+
1086
+ it('应该执行简单的线性流程', async () => {
1087
+ const schedule = createSimpleSchedule();
1088
+ await createTestScheduleInDB(dbCtx, schedule);
1089
+
1090
+ // 配置 mock
1091
+ servicesCtx.mockThinking.configure(schedule.entryNodeId, {
1092
+ plannedNodes: [],
1093
+ });
1094
+ servicesCtx.mockAction.configureAction('my_action', {
1095
+ outputs: { result: 'success' },
1096
+ });
1097
+
1098
+ // 启动 workflow
1099
+ const handle = await testEnv.client.workflow.start(scheduleWorkflow, {
1100
+ args: [{ scheduleId: schedule.id }],
1101
+ taskQueue: 'test-queue',
1102
+ workflowId: `test-${schedule.id}`,
1103
+ });
1104
+
1105
+ await handle.result();
1106
+
1107
+ // 验证
1108
+ const final = await dbCtx.scheduleRepo.getScheduleWithNodes(schedule.id);
1109
+ expect(final?.status).toBe('completed');
1110
+ });
1111
+ });
1112
+ ```
1113
+
1114
+ ### 使用工厂函数创建测试数据
1115
+
1116
+ ```typescript
1117
+ import {
1118
+ createSimpleLinearSchedule,
1119
+ createBranchingSchedule,
1120
+ createLoopingSchedule,
1121
+ } from 'persona-core/tests/helpers/scheduleFactory';
1122
+
1123
+ import {
1124
+ createTestThinkingNode,
1125
+ createTestActionNode,
1126
+ createTestBranchNode,
1127
+ createNodeChain,
1128
+ } from 'persona-core/tests/helpers/nodeFactory';
1129
+
1130
+ // 创建简单线性 Schedule
1131
+ const linearSchedule = createSimpleLinearSchedule('my-linear-schedule');
1132
+
1133
+ // 创建带分支的 Schedule
1134
+ const branchSchedule = createBranchingSchedule('my-branch-schedule');
1135
+
1136
+ // 创建自定义节点
1137
+ const nodes = [
1138
+ createTestThinkingNode('think-1', { thinkingPrompt: '自定义提示' }),
1139
+ createTestActionNode('action-1', 'my_action'),
1140
+ createTestActionNode('action-2', 'another_action'),
1141
+ ];
1142
+ const nodeMap = createNodeChain(nodes);
1143
+ ```
1144
+
1145
+ ---
1146
+
1147
+ ## 高级用法
1148
+
1149
+ ### 动态节点生成
1150
+
1151
+ 思考节点可以在运行时生成新节点:
1152
+
1153
+ ```typescript
1154
+ class DynamicThinkingService implements ThinkingService {
1155
+ async execute(node, context): Promise<ThinkingResult> {
1156
+ // 根据当前状态动态决定后续流程
1157
+ const topics = context.sharedData.data.topics as string[];
1158
+
1159
+ if (topics.length === 0) {
1160
+ // 需要先搜索
1161
+ return {
1162
+ plannedNodes: [
1163
+ {
1164
+ id: 'dynamic-search',
1165
+ kind: 'action',
1166
+ actionTypeId: 'search_topics',
1167
+ inputs: {},
1168
+ outputMappings: [{ outputField: 'topics', targetField: 'topics' }],
1169
+ nextNodeId: 'dynamic-create',
1170
+ },
1171
+ {
1172
+ id: 'dynamic-create',
1173
+ kind: 'action',
1174
+ actionTypeId: 'create_post',
1175
+ inputs: { content: { type: 'reference', fieldName: 'topics' } },
1176
+ outputMappings: [],
1177
+ nextNodeId: null,
1178
+ },
1179
+ ],
1180
+ sharedDataUpdates: {},
1181
+ };
1182
+ }
1183
+
1184
+ // 已有话题,直接创建
1185
+ return {
1186
+ plannedNodes: [
1187
+ {
1188
+ id: 'dynamic-create',
1189
+ kind: 'action',
1190
+ actionTypeId: 'create_post',
1191
+ inputs: { content: { type: 'fixed', value: topics[0] } },
1192
+ outputMappings: [],
1193
+ nextNodeId: null,
1194
+ },
1195
+ ],
1196
+ sharedDataUpdates: {},
1197
+ };
1198
+ }
1199
+ }
1200
+ ```
1201
+
1202
+ ### Workflow 控制信号
1203
+
1204
+ ```typescript
1205
+ // 暂停/恢复
1206
+ await client.pauseSchedule(workflowId);
1207
+ await client.resumeSchedule(workflowId);
1208
+
1209
+ // 查询状态
1210
+ const status = await client.getStatus(workflowId);
1211
+ // {
1212
+ // scheduleId: 'schedule-1',
1213
+ // currentNodeId: 'action-2',
1214
+ // totalIterations: 5,
1215
+ // isPaused: false,
1216
+ // lastNodeResults: [...],
1217
+ // }
1218
+
1219
+ // 取消
1220
+ await client.cancelSchedule(workflowId);
1221
+ ```
1222
+
1223
+ ### 循环检测
1224
+
1225
+ 系统内置循环检测机制:
1226
+
1227
+ - **窗口检测**:检测最近 10 个节点中是否有同一节点出现 6 次及以上(即超过窗口大小的一半)
1228
+ - **警告阈值**:连续触发 5 次警告后终止执行
1229
+ - **最大迭代**:总迭代次数上限为 10000 次
1230
+
1231
+ ### Continue-as-New
1232
+
1233
+ 每执行 100 个节点,Workflow 会自动 Continue-as-New 以避免事件历史过大。运行时状态会保存到数据库并在新 Workflow 中恢复。
1234
+
1235
+ ### 幂等性机制
1236
+
1237
+ 思考节点执行结果会被缓存,确保在 Temporal 重试或 Continue-as-New 时不会重复执行。
1238
+
1239
+ **幂等性键格式**:`{scheduleId}:{nodeId}:{executionSeq}`
1240
+
1241
+ 其中 `executionSeq` 是 Schedule 的执行序号,每次节点执行后递增。
1242
+
1243
+ **工作原理**:
1244
+ 1. 执行思考节点前,检查是否存在相同幂等性键的结果
1245
+ 2. 如果存在,直接返回缓存的结果
1246
+ 3. 如果不存在,执行思考逻辑并缓存结果
1247
+
1248
+ 这确保了:
1249
+ - Temporal Activity 重试时不会重复调用 LLM
1250
+ - Continue-as-New 后不会重复规划
1251
+
1252
+ ### 验证 Schedule 结构
1253
+
1254
+ ```typescript
1255
+ import { validateLoops, validateScheduleStructure } from 'persona-core';
1256
+
1257
+ // 验证循环
1258
+ const loopResults = validateLoops(schedule);
1259
+ for (const result of loopResults) {
1260
+ if (!result.isValid) {
1261
+ console.error('无效循环:', result.error);
1262
+ console.error('循环路径:', result.loopPath);
1263
+ }
1264
+ }
1265
+
1266
+ // 验证整体结构
1267
+ const structureResult = validateScheduleStructure(schedule);
1268
+ if (!structureResult.isValid) {
1269
+ console.error('结构错误:', structureResult.errors);
1270
+ }
1271
+ ```
1272
+
1273
+ ---
1274
+
1275
+ ## 错误处理
1276
+
1277
+ ### 状态转换
1278
+
1279
+ Schedule 状态遵循严格的转换规则:
1280
+
1281
+ ```
1282
+ pending → running, cancelled
1283
+ running → paused, completed, failed, cancelled
1284
+ paused → running, cancelled
1285
+ completed, failed, cancelled → (终态,不可转换)
1286
+ ```
1287
+
1288
+ ### 节点失败策略
1289
+
1290
+ 节点执行失败时的默认行为是终止整个 Schedule。系统会:
1291
+ 1. 记录节点执行失败信息
1292
+ 2. 将 Schedule 状态更新为 `failed`
1293
+ 3. 抛出 `ApplicationFailure` 错误
1294
+
1295
+ ### Activity 超时与重试配置
1296
+
1297
+ 各类 Activity 的默认配置:
1298
+
1299
+ | Activity 类型 | startToCloseTimeout | 最大重试次数 | 初始重试间隔 |
1300
+ |--------------|---------------------|-------------|-------------|
1301
+ | DB | 5s | 3 | 100ms |
1302
+ | Thinking | 60s | 2 | 1s |
1303
+ | Action | 30s | 3 | 500ms |
1304
+ | Branch | 10s | 3 | 100ms |
1305
+
1306
+ 所有 Activity 使用指数退避(backoffCoefficient: 2)进行重试。
1307
+
1308
+ ### 常见错误
1309
+
1310
+ | 错误类型 | 说明 |
1311
+ |---------|------|
1312
+ | `InvalidStatusTransitionError` | 非法状态转换 |
1313
+ | `RateLimitError` | API 限流 |
1314
+ | `InvalidResponseError` | LLM 返回无效响应 |
1315
+ | `ApplicationFailure.nonRetryable` | 不可重试的业务错误 |
1316
+
1317
+ ---
1318
+
1319
+ ## 最佳实践
1320
+
1321
+ ---
1322
+
1323
+ ## Template Loader 表达式
1324
+
1325
+ ### 概述
1326
+
1327
+ Template Loader 是一种在 API 调用模板中读取本地文件的能力。当你需要将一个节点生成的文件(如图片)作为另一个 API 调用的输入时,这个功能非常有用。
1328
+
1329
+ ### 使用场景
1330
+
1331
+ 1. **图生视频**:使用生图 API 生成图片保存到本地 → 使用视频生成 API 将图片作为首帧
1332
+ 2. **图像处理链**:生成图片 → 读取图片进行风格转换 → 再次处理
1333
+ 3. **文档生成**:生成文本文件 → 作为附件发送 API
1334
+
1335
+ ### 语法
1336
+
1337
+ ```
1338
+ ${loaderType:fieldName|transformer1|transformer2|...}
1339
+ ```
1340
+
1341
+ | 部分 | 说明 | 示例 |
1342
+ |------|------|------|
1343
+ | `loaderType:` | 前缀标识符,表明这是一个 loader 表达式 | `file:` |
1344
+ | `fieldName` | 输入字段名,其值应为文件路径 | `imagePath` |
1345
+ | `\|transformer` | 可选的转换器链,使用 `\|` 分隔 | `\|base64\|dataUri` |
1346
+
1347
+ ### 支持的 Loader
1348
+
1349
+ #### `file:` - 文件读取 Loader
1350
+
1351
+ 读取本地文件内容:
1352
+
1353
+ ```
1354
+ ${file:imagePath} # 读取文件,返回文本内容
1355
+ ${file:imagePath|base64} # 读取文件,返回 base64 编码
1356
+ ${file:imagePath|base64|dataUri} # 读取文件,返回 data URI 格式
1357
+ ```
1358
+
1359
+ ### 支持的 Transformer
1360
+
1361
+ | Transformer | 输入 | 输出 | 说明 |
1362
+ |-------------|------|------|------|
1363
+ | `base64` | Buffer/string | string | 编码为 base64 |
1364
+ | `dataUri` | base64 string | string | 添加 data URI 前缀(如 `data:image/png;base64,...`) |
1365
+ | `json` | any | string | JSON 序列化 |
1366
+ | `trim` | string | string | 去除首尾空白 |
1367
+
1368
+ ### 完整示例:图生视频
1369
+
1370
+ **动作类型定义:**
1371
+
1372
+ ```typescript
1373
+ {
1374
+ kind: 'action',
1375
+ id: 'generate_video_from_image',
1376
+ name: '图生视频',
1377
+ executionType: 'api_call',
1378
+ apiConfig: {
1379
+ url: 'https://api.example.com/video/generate',
1380
+ method: 'POST',
1381
+ headers: {
1382
+ type: 'static',
1383
+ values: {
1384
+ 'Content-Type': 'application/json',
1385
+ 'Authorization': 'Bearer ${apiKey}',
1386
+ },
1387
+ },
1388
+ body: {
1389
+ contentType: 'template',
1390
+ template: JSON.stringify({
1391
+ model: 'video-model',
1392
+ content: [
1393
+ {
1394
+ type: 'image_url',
1395
+ image_url: {
1396
+ url: '${file:sourceImagePath|base64|dataUri}' // 🔑 使用 loader 语法
1397
+ },
1398
+ role: 'first_frame'
1399
+ },
1400
+ {
1401
+ type: 'text',
1402
+ text: '${prompt}'
1403
+ }
1404
+ ]
1405
+ })
1406
+ }
1407
+ },
1408
+ inputFields: [
1409
+ { name: 'sourceImagePath', required: true, typeHint: 'string', description: '源图片文件路径' },
1410
+ { name: 'prompt', required: true, typeHint: 'string', description: '动态描述' },
1411
+ ],
1412
+ // ...
1413
+ }
1414
+ ```
1415
+
1416
+ **工作流节点配置:**
1417
+
1418
+ ```typescript
1419
+ // 节点 1: 生成图片
1420
+ {
1421
+ id: 'generate-image',
1422
+ kind: 'action',
1423
+ actionTypeId: 'generate_comic_art',
1424
+ inputs: {
1425
+ prompt: { type: 'fixed', value: 'A hero standing...' },
1426
+ outputPath: { type: 'fixed', value: 'output/hero.png' }
1427
+ },
1428
+ outputMappings: [
1429
+ { outputField: 'savedFilePath', targetField: 'generatedImagePath' }
1430
+ ],
1431
+ nextNodeId: 'generate-video'
1432
+ }
1433
+
1434
+ // 节点 2: 使用图片生成视频
1435
+ {
1436
+ id: 'generate-video',
1437
+ kind: 'action',
1438
+ actionTypeId: 'generate_video_from_image',
1439
+ inputs: {
1440
+ sourceImagePath: { type: 'reference', fieldName: 'generatedImagePath' }, // 引用上一步的图片路径
1441
+ prompt: { type: 'fixed', value: 'The hero raises sword...' }
1442
+ },
1443
+ // ...
1444
+ }
1445
+ ```
1446
+
1447
+ ### 安全考虑
1448
+
1449
+ 1. **路径遍历防护**:文件路径必须在配置的 baseDir 目录内
1450
+ 2. **敏感文件保护**:禁止读取 `.env`、`keys.json`、`.pem` 等敏感文件
1451
+ 3. **文件大小限制**:默认最大 50MB,可通过 `FileOperationConfig.maxFileSize` 配置
1452
+
1453
+ ### 向后兼容
1454
+
1455
+ 普通变量语法 `${fieldName}` 保持不变:
1456
+
1457
+ - 只有明确使用 `${loader:...}` 语法时才触发新逻辑
1458
+ - 现有 api_call 配置完全兼容
1459
+
1460
+ ---
1461
+
1462
+ ## 最佳实践
1463
+
1464
+ 1. **Persona 设计**
1465
+ - 保持 System Prompt 简洁明确
1466
+ - 动作类型的输入输出定义要完整
1467
+
1468
+ 2. **Schedule 设计**
1469
+ - 第一个节点必须是思考节点
1470
+ - 循环中必须有可退出的分支
1471
+ - 合理使用共享数据的集合类型
1472
+
1473
+ 3. **服务实现**
1474
+ - 思考服务应该具有幂等性
1475
+ - 动作服务应该处理好重试逻辑
1476
+ - 分支服务的决策应该是确定性的
1477
+
1478
+ 4. **测试**
1479
+ - 使用 Mock 服务进行单元测试
1480
+ - 使用 TestWorkflowEnvironment 进行集成测试
1481
+ - 测试循环和分支的边界情况
1482
+
1483
+ 5. **生产部署**
1484
+ - 使用 PostgreSQL 替代 PGlite
1485
+ - 配置合适的 Temporal 重试策略
1486
+ - 监控 Workflow 执行状态和循环警告
1487
+