pasm-framework 0.3.0__tar.gz → 0.4.0__tar.gz

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 (36) hide show
  1. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/PKG-INFO +48 -5
  2. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/README.md +46 -2
  3. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/__init__.py +40 -1
  4. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/__main__.py +86 -1
  5. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/application.py +37 -5
  6. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/demo.py +6 -0
  7. pasm_framework-0.4.0/pasm_framework/plugins/builtins/_web_assets.py +509 -0
  8. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/knowledge_base.py +51 -1
  9. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/sessions.py +22 -0
  10. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/web_gateway.py +292 -18
  11. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/PKG-INFO +48 -5
  12. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/SOURCES.txt +1 -0
  13. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pyproject.toml +5 -4
  14. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/LICENSE +0 -0
  15. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/adapter.py +0 -0
  16. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/config.py +0 -0
  17. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/discovery.py +0 -0
  18. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/errors.py +0 -0
  19. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/__init__.py +0 -0
  20. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/__init__.py +0 -0
  21. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/llm_responder.py +0 -0
  22. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/observability.py +0 -0
  23. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/safety.py +0 -0
  24. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/warmth.py +0 -0
  25. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/core.py +0 -0
  26. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/registry.py +0 -0
  27. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/py.typed +0 -0
  28. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/scaffold.py +0 -0
  29. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/service.py +0 -0
  30. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/simple.py +0 -0
  31. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/skill.py +0 -0
  32. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/dependency_links.txt +0 -0
  33. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/entry_points.txt +0 -0
  34. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/requires.txt +0 -0
  35. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/top_level.txt +0 -0
  36. {pasm_framework-0.3.0 → pasm_framework-0.4.0}/setup.cfg +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pasm-framework
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: PASM 应用开发框架 —— 产品智能体/应用与认知引擎之间的防腐层(V1↔V2 唯一变动点)
5
5
  Author: arronzheng
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://gitee.com/arronzheng/pasm-framework
8
8
  Project-URL: Repository, https://github.com/arronJack/pasm-framework
9
9
  Project-URL: Source, https://gitee.com/arronzheng/pasm-framework
@@ -11,7 +11,6 @@ Project-URL: Issues, https://gitee.com/arronzheng/pasm-framework/issues
11
11
  Keywords: pasm,agent,application-framework,anti-corruption-layer,cognitive,capability,skill,domain-adapter
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Operating System :: OS Independent
16
15
  Classifier: Programming Language :: Python :: 3
17
16
  Classifier: Programming Language :: Python :: 3.10
@@ -54,7 +53,7 @@ PASM V1 → V2 升级时,**不重写 4 个产品智能体 + 3 个技能**。
54
53
 
55
54
  `CognitiveBackend` 协议的**单一真相源仍在基座** `pasm_skills.sdk.backend`,本包只做重导出。
56
55
 
57
- ## 流式与工具调用(v0.3.0 新增)
56
+ ## 流式与工具调用(v0.3.0)
58
57
 
59
58
  **真流式(SSE)** —— `stream()` 与 `handle()` **共用同一条管线**,不会行为漂移:
60
59
 
@@ -163,6 +162,48 @@ sla = "my_pkg.my_module:SlaPlugin"
163
162
  ⚠️ 插件名**拼错不会静默通过**:`app.plugins.unknown()`、`app.app_summary()["plugin_config_unknown"]`
164
163
  以及 `pasm-framework doctor` 都会报告。
165
164
 
165
+ ## 一键起在线客服(v0.4.0)
166
+
167
+ **一条命令**拿到:访客对话页 + 站长管理台 + 一行嵌入代码 + REST 示例(双令牌自动生成)。
168
+
169
+ ```bash
170
+ pasm-framework cs --port 8080 --name "小智客服"
171
+ ```
172
+
173
+ 输出会直接给你三段:
174
+
175
+ ```
176
+ ① 访客对话页 http://127.0.0.1:8080/
177
+ ② 站长管理台 http://127.0.0.1:8080/console?token=<管理令牌>
178
+ ③ 嵌入任意语言站点 <script src=".../embed.js" data-pasm-token="<公开令牌>" ...></script>
179
+ ```
180
+
181
+ **站点接入只要一行**(PHP / Java / Node / 静态页都行):
182
+
183
+ ```html
184
+ <script src="http://你的服务器:8080/embed.js"
185
+ data-pasm-token="公开令牌"
186
+ data-title="在线客服"
187
+ data-greeting="你好!我是智能客服,有什么可以帮你?"
188
+ data-color="#2563eb" defer></script>
189
+ ```
190
+
191
+ > ⚠️ 属性名必须是 **`data-pasm-token`**(写成 `data-token` 挂件拿不到令牌 → 401)。
192
+
193
+ **双令牌作用域**(安全红线,selftest 已固化):
194
+
195
+ | 令牌 | 能做什么 |
196
+ | --- | --- |
197
+ | `token`(管理) | 全部接口:对话 + 导入资料 + 看会话 + 管理台 |
198
+ | `public_token`(访客) | **仅** `/api/chat`、`/api/chat/stream`;配了它时挂件页对访客开放 |
199
+
200
+ `serve_token` **只返回 `public_token`,绝不回落管理令牌** —— 挂件页是给访客看的,
201
+ 一旦回落,源码里就能读到后台令牌。未配 `public_token` 时挂件页本身也不公开。
202
+ `/embed.js` 与 `/healthz` 是唯一无条件公开的路径(本身不含密钥)。
203
+
204
+ 管理台里可以**粘贴整篇帮助文档**(`POST /api/ingest/text`)自动切块、识别「问:/答:」存成问答对,
205
+ 访客立刻就能问到 —— 这就是"资料库自学"的入口。
206
+
166
207
  ## 参考实现:站点智能客服
167
208
 
168
209
  `apps/customer_service.py` 用插件组合出一个可上线的客服 Agent(零 LLM 也能回答)。
@@ -258,7 +299,7 @@ print(app.handle("你好")) # → "reply:你好"(回落 chat)
258
299
  ## 快速自检
259
300
 
260
301
  ```bash
261
- python -m pasm_framework selftest # 44 项
302
+ python -m pasm_framework selftest # 55 项
262
303
  python -m pasm_framework doctor # 真装配一遍插件并报拼错的名字
263
304
  python -m pasm_framework version
264
305
  ```
@@ -273,6 +314,8 @@ python -m pasm_framework version
273
314
  | [`docs/cross-platform-strategy.md`](docs/cross-platform-strategy.md) | 桌面端跨 Linux/macOS、手机端换架构方案 |
274
315
  | [`docs/customer-service-plugin.md`](docs/customer-service-plugin.md) | 智能客服可行性与部署形态研究 |
275
316
  | [`docs/audit-2026-09-20.md`](docs/audit-2026-09-20.md) | v0.2.0 体检报告 |
317
+ | [`docs/plugin-ownership.md`](docs/plugin-ownership.md) | 插件放子目录还是独立仓(决策 + 理由) |
318
+ | [`docs/audit-7repos-2026-09-20.md`](docs/audit-7repos-2026-09-20.md) | **七仓全盘审计**(含跨平台实测) |
276
319
  | [`docs/openapi.yaml`](docs/openapi.yaml) | HTTP 契约(单一真相源) |
277
320
  | [`sdks/`](sdks/README.md) | C# / Java / PHP / Node / Go / Python 客户端 |
278
321
 
@@ -26,7 +26,7 @@ PASM V1 → V2 升级时,**不重写 4 个产品智能体 + 3 个技能**。
26
26
 
27
27
  `CognitiveBackend` 协议的**单一真相源仍在基座** `pasm_skills.sdk.backend`,本包只做重导出。
28
28
 
29
- ## 流式与工具调用(v0.3.0 新增)
29
+ ## 流式与工具调用(v0.3.0)
30
30
 
31
31
  **真流式(SSE)** —— `stream()` 与 `handle()` **共用同一条管线**,不会行为漂移:
32
32
 
@@ -135,6 +135,48 @@ sla = "my_pkg.my_module:SlaPlugin"
135
135
  ⚠️ 插件名**拼错不会静默通过**:`app.plugins.unknown()`、`app.app_summary()["plugin_config_unknown"]`
136
136
  以及 `pasm-framework doctor` 都会报告。
137
137
 
138
+ ## 一键起在线客服(v0.4.0)
139
+
140
+ **一条命令**拿到:访客对话页 + 站长管理台 + 一行嵌入代码 + REST 示例(双令牌自动生成)。
141
+
142
+ ```bash
143
+ pasm-framework cs --port 8080 --name "小智客服"
144
+ ```
145
+
146
+ 输出会直接给你三段:
147
+
148
+ ```
149
+ ① 访客对话页 http://127.0.0.1:8080/
150
+ ② 站长管理台 http://127.0.0.1:8080/console?token=<管理令牌>
151
+ ③ 嵌入任意语言站点 <script src=".../embed.js" data-pasm-token="<公开令牌>" ...></script>
152
+ ```
153
+
154
+ **站点接入只要一行**(PHP / Java / Node / 静态页都行):
155
+
156
+ ```html
157
+ <script src="http://你的服务器:8080/embed.js"
158
+ data-pasm-token="公开令牌"
159
+ data-title="在线客服"
160
+ data-greeting="你好!我是智能客服,有什么可以帮你?"
161
+ data-color="#2563eb" defer></script>
162
+ ```
163
+
164
+ > ⚠️ 属性名必须是 **`data-pasm-token`**(写成 `data-token` 挂件拿不到令牌 → 401)。
165
+
166
+ **双令牌作用域**(安全红线,selftest 已固化):
167
+
168
+ | 令牌 | 能做什么 |
169
+ | --- | --- |
170
+ | `token`(管理) | 全部接口:对话 + 导入资料 + 看会话 + 管理台 |
171
+ | `public_token`(访客) | **仅** `/api/chat`、`/api/chat/stream`;配了它时挂件页对访客开放 |
172
+
173
+ `serve_token` **只返回 `public_token`,绝不回落管理令牌** —— 挂件页是给访客看的,
174
+ 一旦回落,源码里就能读到后台令牌。未配 `public_token` 时挂件页本身也不公开。
175
+ `/embed.js` 与 `/healthz` 是唯一无条件公开的路径(本身不含密钥)。
176
+
177
+ 管理台里可以**粘贴整篇帮助文档**(`POST /api/ingest/text`)自动切块、识别「问:/答:」存成问答对,
178
+ 访客立刻就能问到 —— 这就是"资料库自学"的入口。
179
+
138
180
  ## 参考实现:站点智能客服
139
181
 
140
182
  `apps/customer_service.py` 用插件组合出一个可上线的客服 Agent(零 LLM 也能回答)。
@@ -230,7 +272,7 @@ print(app.handle("你好")) # → "reply:你好"(回落 chat)
230
272
  ## 快速自检
231
273
 
232
274
  ```bash
233
- python -m pasm_framework selftest # 44 项
275
+ python -m pasm_framework selftest # 55 项
234
276
  python -m pasm_framework doctor # 真装配一遍插件并报拼错的名字
235
277
  python -m pasm_framework version
236
278
  ```
@@ -245,6 +287,8 @@ python -m pasm_framework version
245
287
  | [`docs/cross-platform-strategy.md`](docs/cross-platform-strategy.md) | 桌面端跨 Linux/macOS、手机端换架构方案 |
246
288
  | [`docs/customer-service-plugin.md`](docs/customer-service-plugin.md) | 智能客服可行性与部署形态研究 |
247
289
  | [`docs/audit-2026-09-20.md`](docs/audit-2026-09-20.md) | v0.2.0 体检报告 |
290
+ | [`docs/plugin-ownership.md`](docs/plugin-ownership.md) | 插件放子目录还是独立仓(决策 + 理由) |
291
+ | [`docs/audit-7repos-2026-09-20.md`](docs/audit-7repos-2026-09-20.md) | **七仓全盘审计**(含跨平台实测) |
248
292
  | [`docs/openapi.yaml`](docs/openapi.yaml) | HTTP 契约(单一真相源) |
249
293
  | [`sdks/`](sdks/README.md) | C# / Java / PHP / Node / Go / Python 客户端 |
250
294
 
@@ -39,7 +39,7 @@ PASM V1→V2 升级时,**不重写 4 个产品智能体 + 3 个技能**。手
39
39
  """
40
40
  from __future__ import annotations
41
41
 
42
- __version__ = "0.3.0"
42
+ __version__ = "0.4.0"
43
43
 
44
44
  from .adapter import ( # noqa: F401
45
45
  DomainAdapter,
@@ -292,6 +292,45 @@ def selftest() -> bool:
292
292
  gw = pm.get("web_gateway")
293
293
  check(gw is not None, "web_gateway 插件可构造(不启动服务器)")
294
294
 
295
+ # ---- v0.3.1 站点插件安全不变式(不需要起服务器)----
296
+ # `_Handler._gw` 读的是 `self.server.gateway`,所以给个假 server 就能
297
+ # 直接调鉴权逻辑,无需真的监听端口。
298
+ from .plugins.builtins.web_gateway import _Handler as _GWHandler
299
+
300
+ class _ProbeHandler(_GWHandler):
301
+ def __init__(self, gateway, path, headers=None):
302
+ self.server = type("_S", (), {"gateway": gateway})()
303
+ self.path = path
304
+ self.headers = headers or {}
305
+
306
+ def _probe(gateway, path, tok=None):
307
+ h = _ProbeHandler(gateway, path,
308
+ {"Authorization": "Bearer " + tok} if tok else {})
309
+ return h._authorized(path)
310
+
311
+ _g = type(gw)({"token": "ADMIN", "public_token": "PUB"})
312
+ check(_probe(_g, "/console", "ADMIN") and not _probe(_g, "/console", "PUB"),
313
+ "网关:管理台只认管理令牌(公开令牌被拒)")
314
+ check(not _probe(_g, "/console?token=PUB", "PUB"),
315
+ "网关:?token= 携带公开令牌也进不了管理台")
316
+ _h = _ProbeHandler(_g, "/api/chat", {"Authorization": "Bearer PUB"})
317
+ check(_h._authorized("/api/chat", admin=False)
318
+ and not _h._authorized("/api/chat", admin=True),
319
+ "网关:公开令牌仅限对话作用域")
320
+ check(_probe(_g, "/embed.js") and _probe(_g, "/healthz"),
321
+ "网关:/embed.js 与 /healthz 无条件可达")
322
+ _g2 = type(gw)({"token": "ADMIN"}) # 没配 public_token
323
+ check(_g2.serve_token == "",
324
+ "★ 未配 public_token 时 serve_token 为空(绝不回落管理令牌)")
325
+ check(_ProbeHandler(_g2, "/")._authorized("/") is False,
326
+ "★ 未配 public_token 时挂件页不公开(不泄漏管理令牌)")
327
+ check(_ProbeHandler(_g2, "/",
328
+ {"Authorization": "Bearer ADMIN"})._authorized("/"),
329
+ "网关:站长用管理令牌仍可打开挂件页")
330
+ _g3 = type(gw)({}) # 无令牌 = 开发开放模式
331
+ check(_ProbeHandler(_g3, "/console")._authorized("/console"),
332
+ "网关:无令牌配置时保持开放(兼容本机开发)")
333
+
295
334
  # ---- v0.2.1 收尾阶段不变式 ----
296
335
  # 1) on_reply_final 对**每条**回复路径恰好跑一次(生成/能力/模板)。
297
336
  class _Probe(BasePlugin):
@@ -6,8 +6,9 @@
6
6
  pasm-framework plugins 列出内置插件与默认开关
7
7
  pasm-framework config [--preset X | --file f.json] [--env] 查看解析后的开关表
8
8
  pasm-framework doctor 环境体检(能不能跑、缺什么)
9
- pasm-framework new <dir> [--kind app|chatbot|game_npc] [--force] 生成项目
9
+ pasm-framework new <dir> [--kind app|chatbot|game_npc|customer_service] [--force] 生成项目
10
10
  pasm-framework serve [--port 8080] [--host 0.0.0.0] [--kb ./kb] 起一个演示客服
11
+ pasm-framework cs [--port 8080] [--name X] 一键起在线客服(双令牌+嵌入码+管理台)
11
12
 
12
13
  设计约束:只用标准库 ``argparse``,不引入 CLI 框架 —— 保证"装完就能跑"。
13
14
  """
@@ -210,6 +211,74 @@ def _cmd_serve(args) -> int:
210
211
  return 0
211
212
 
212
213
 
214
+ def _cmd_cs(args) -> int:
215
+ """一键起在线客服:自动生成双令牌,打印可分享链接与嵌入代码。
216
+
217
+ 面向"不懂代码也能跑":一条命令拿到
218
+ · 访客对话页(可直接分享 / iframe 嵌入)
219
+ · 站长管理台(粘贴资料 → 立即生效)
220
+ · 一行 <script> 嵌入代码(任意语言开发的站点都能挂)
221
+ """
222
+ import time
223
+ import secrets
224
+
225
+ from .demo import make_demo_app
226
+
227
+ serve_token = args.serve_token or secrets.token_hex(8)
228
+ public_token = args.public_token or secrets.token_hex(8)
229
+
230
+ llm = None
231
+ if args.llm_provider:
232
+ llm = {"provider": args.llm_provider}
233
+ if args.llm_model:
234
+ llm["model"] = args.llm_model
235
+ if args.llm_key:
236
+ llm["api_key"] = args.llm_key
237
+
238
+ app = make_demo_app(kb_dir=args.kb, llm=llm, host=args.host, port=args.port,
239
+ token=serve_token, public_token=public_token,
240
+ preset_name="chatbot")
241
+
242
+ shown_host = args.host if args.host != "0.0.0.0" else "127.0.0.1"
243
+ base = "http://%s:%d" % (shown_host, args.port)
244
+ lan = "http://<你的局域网IP>:%d" % args.port if args.host == "0.0.0.0" else base
245
+ # 属性名必须与 embed.js 读取的一致(data-pasm-token),否则挂件拿不到令牌。
246
+ embed = ("<script src=\"%s/embed.js\"\n"
247
+ " data-pasm-token=\"%s\"\n"
248
+ " data-title=\"%s\"\n"
249
+ " data-greeting=\"你好!我是智能客服,有什么可以帮你?\"\n"
250
+ " data-color=\"#2563eb\" defer></script>"
251
+ % (lan, public_token, args.name))
252
+
253
+ print("=" * 62)
254
+ print("在线客服已启动(%s)" % args.name)
255
+ print("=" * 62)
256
+ print("① 访客对话页(分享给客户 / 直接打开测试):")
257
+ print(" %s/" % base)
258
+ print("② 站长管理台(粘贴资料 → 访客立即能答):")
259
+ print(" %s/console?token=%s" % (base, serve_token))
260
+ print(" (链接已带管理令牌;打开后令牌会从地址栏消失,存进本次会话)")
261
+ print("③ 嵌入你自己的站点(任意语言:PHP/Java/Node/静态页均可):")
262
+ print(" 在 </body> 前加这几行:")
263
+ print(" %s" % embed.replace("\n", "\n "))
264
+ print("④ REST 接口(程序对接):")
265
+ print(" POST %s/api/chat {\"text\": \"...\"}" % base)
266
+ print(" Header: Authorization: Bearer %s" % public_token)
267
+ print("-" * 62)
268
+ print("令牌说明:公开令牌只能对话;管理令牌才能导入资料/看会话。")
269
+ print("资料来源:启动时自动导入演示 FAQ;往 %s 放 .txt/.md 或在管理台粘贴即可。" % (args.kb or "~/.pasm_framework/kb"))
270
+ print("Ctrl+C 退出")
271
+ print("=" * 62)
272
+ try:
273
+ app.serve()
274
+ while True:
275
+ time.sleep(1)
276
+ except KeyboardInterrupt:
277
+ print("\n已停止。")
278
+ app.close()
279
+ return 0
280
+
281
+
213
282
  def main(argv=None) -> int:
214
283
  argv = list(sys.argv[1:] if argv is None else argv)
215
284
  cmd = argv[0] if argv else "selftest"
@@ -260,6 +329,21 @@ def main(argv=None) -> int:
260
329
  p_srv.add_argument("--llm-model", default=None)
261
330
  p_srv.add_argument("--llm-key", default=None)
262
331
 
332
+ p_cs = sub.add_parser("cs", help="一键起在线客服(双令牌 + 可分享链接 + 嵌入代码)")
333
+ p_cs.add_argument("--name", default="在线客服", help="客服名(打印在横幅里)")
334
+ p_cs.add_argument("--host", default="127.0.0.1",
335
+ help="0.0.0.0 表示允许局域网访问")
336
+ p_cs.add_argument("--port", type=int, default=8080)
337
+ p_cs.add_argument("--kb", default=None, help="知识库目录(默认 ~/.pasm_framework/kb)")
338
+ p_cs.add_argument("--serve-token", dest="serve_token", default=None,
339
+ help="管理令牌(缺省自动生成)")
340
+ p_cs.add_argument("--public-token", dest="public_token", default=None,
341
+ help="公开令牌(缺省自动生成)")
342
+ p_cs.add_argument("--llm-provider", default=None,
343
+ help="openai / deepseek / ollama(不填则离线模式)")
344
+ p_cs.add_argument("--llm-model", default=None)
345
+ p_cs.add_argument("--llm-key", default=None)
346
+
263
347
  try:
264
348
  ns = parser.parse_args(argv)
265
349
  except SystemExit as ex: # argparse 的报错也是退出
@@ -271,6 +355,7 @@ def main(argv=None) -> int:
271
355
  "doctor": _cmd_doctor,
272
356
  "new": _cmd_new,
273
357
  "serve": _cmd_serve,
358
+ "cs": _cmd_cs,
274
359
  }.get(ns.sub)
275
360
  if handler is None:
276
361
  print(USAGE)
@@ -106,8 +106,11 @@ class BaseApplication(BaseAgent):
106
106
  去重按 ``title``;凡"有依据的知识"(领域 / 知识库)都带 ``source``,
107
107
  纯对话记忆(episodic)不带 —— 应用可据此区分"资料"与"闲聊"。
108
108
  """
109
- hits = super().recall(query, k=k)
110
- seen = {h.get("title") for h in hits}
109
+ # 对话记忆(episodic)—— 不带 source。
110
+ memories = super().recall(query, k=k)
111
+ seen = {h.get("title") for h in memories}
112
+ # 「有依据的知识」单独收集:领域 + 知识库。
113
+ sourced: List[Dict[str, Any]] = []
111
114
 
112
115
  if hasattr(self.domain, "knowledge_for"):
113
116
  try:
@@ -118,7 +121,7 @@ class BaseApplication(BaseAgent):
118
121
  it = dict(item)
119
122
  it.setdefault("source",
120
123
  "domain:%s" % type(self.domain).__name__)
121
- hits.append(it)
124
+ sourced.append(it)
122
125
  seen.add(it.get("title"))
123
126
  except Exception:
124
127
  pass
@@ -132,12 +135,41 @@ class BaseApplication(BaseAgent):
132
135
  for item in kb.recall(query, k=k):
133
136
  if item.get("title") in seen:
134
137
  continue
135
- hits.append(item)
138
+ sourced.append(item)
136
139
  seen.add(item.get("title"))
137
140
  except Exception:
138
141
  pass
139
142
 
140
- return hits[:k] if k else hits
143
+ # 排序:**有依据的知识优先于对话记忆**。
144
+ # 否则"上次聊过一句提到过某个词"的闲聊记忆会排到 FAQ 前面,
145
+ # 客服就变成"答非所问"(实测过:闲聊记忆抢占,资料库排到了后面)。
146
+ merged = sourced + memories
147
+ return merged[:k] if k else merged
148
+
149
+ # ---- 区分「资料」与「闲聊」---------------------------------
150
+ def knowledge_facts(
151
+ self, facts: Optional[List[Dict[str, Any]]]
152
+ ) -> List[Dict[str, Any]]:
153
+ """从检索结果里挑出**有依据的知识**(带 ``source`` 的那些)。
154
+
155
+ 为什么必须有这个方法:``recall()`` 返回的是**混合**结果 ——
156
+ 引擎的对话记忆(episodic,不带 ``source``)+ 领域知识 + 知识库资料
157
+ (都带 ``source``)。写 ``_render_reply`` 时如果直接取 ``facts[0]``
158
+ 就会把"上一轮用户自己说过的话"当成资料答回去。
159
+
160
+ 实测过的翻车现场:用户问"你们老板叫什么",
161
+ 系统把上一轮的"积分怎么算"当成资料答了出来 ——
162
+ 因为那条对话记忆被检索命中,而它**不是知识**。
163
+
164
+ 所以任何"就资料作答"的回复策略都应该先过这个过滤器::
165
+
166
+ def _render_reply(self, text, facts, mood):
167
+ k = self.knowledge_facts(facts)
168
+ return k[0]["brief"] if k else "抱歉,我暂时没有相关资料。"
169
+
170
+ 保持原顺序(资料在前、且各自内部已按相关度排序),只做过滤不做重排。
171
+ """
172
+ return [f for f in (facts or []) if f.get("source")]
141
173
 
142
174
  # ---- 对话(覆盖基类,支持注入已检索事实)-----------------
143
175
  def chat(self, text: str, facts: Optional[List[Dict[str, Any]]] = None) -> str:
@@ -53,10 +53,14 @@ def make_demo_app(
53
53
  agent_id: str = "pasm-demo",
54
54
  preset_name: str = "chatbot",
55
55
  token: Optional[str] = None,
56
+ public_token: Optional[str] = None,
56
57
  teach: bool = True,
57
58
  ) -> DemoAgent:
58
59
  """构造演示应用(已导入 :data:`DEMO_FAQ`)。
59
60
 
61
+ ``token`` 是**管理令牌**(全部接口);``public_token`` 是**访客令牌**
62
+ (仅对话接口,可安全嵌进第三方站点)。只给 ``token`` 时行为同旧版。
63
+
60
64
  >>> from pasm_framework.demo import make_demo_app
61
65
  >>> app = make_demo_app(port=8099)
62
66
  >>> "退货" in app.ask("怎么退货?")
@@ -65,6 +69,8 @@ def make_demo_app(
65
69
  gw: Dict[str, Any] = {"enabled": True, "config": {"host": host, "port": port}}
66
70
  if token:
67
71
  gw["config"]["token"] = token
72
+ if public_token:
73
+ gw["config"]["public_token"] = public_token
68
74
  kb: Dict[str, Any] = {"enabled": True}
69
75
  if kb_dir:
70
76
  kb["config"] = {"kb_dir": kb_dir}