Commit Graph
97 Commits
Author SHA1 Message Date
qing 2c6d1e8ce6 docs: record round-5 audit and batch K fixes in aibox report and README
审计报告追加第五轮(§13 发现 J1~J16 + §14 波次 1~5 落地记录),
README 同步测试清单与计数(490 项全过)。
2026-08-19 07:01:22 +08:00
qing 457bb900fb fix(aibox): round-5 audit batch K - max_turns clamp, task input validation, XSS hardening, atomic writes, broadcast timeout
第五轮审计(J1~J16)全量修复 + 子代理 max_turns 钳制:
- 子代理 max_turns 钳制(0/None 回退默认 10,负数钳制 1)+ 14 项回归测试
- J1 任务系统投毒:add_memo 入库校验(时间/间隔/类型)、调度器预检与触发事务
  防御、展示路径兜底、register 移入 try、add_user_task 补 repeat_end_time 透传
- J2/J13 前端 XSS:三页面 marked 输出经 DOMPurify 净化、entry1 系统消息/任务
  分隔条改 textContent、CDN 脚本补 SRI
- J3 FTS 删除换序(先 fts 后内容表,索引不再残留)
- J5 原子写推广:新增 common/atomic.py(mkstemp+fsync+os.replace),config/prompt/
  history 全部接入;损坏配置备份后回退
- J6 单例初始化加锁(PromptManager/ConfigManager 并发首例不双重加载)
- J7 broadcast 并发发送 + wait_for 超时回收僵死连接
- J4/J8/J9~J12/J14~J16:验证异常脱敏、交互输入容错、MemoDatabase WAL+busy_timeout、
  memo 批量删除、/new 竞态、skills 引号转义、voice_client 清理、死代码移除、
  skill_manager 同名键/help 截断
2026-08-19 07:01:13 +08:00
qing d513c5f892 feat(aibox): interactive model profile manager (model_config.py)
Menu-driven list/add/delete/connectivity-test for config.json models
section - no hand-written JSON needed. Key convention preserved: env
var name only, never plaintext (or empty = keyless local endpoint).
Delete auto-cleans dangling fallback refs; default profile via
api.default_model; save failure reported. 17 unit tests, 441 passed.
2026-08-18 03:26:53 +00:00
qing 4cb97318b5 docs: sync aibox audit batches G/H/I in root README and aibox README
- root README: aibox section notes four audit rounds (Batch E/F/G/H/I)
- aibox README: v3.5 changelog (Batch G/H/I), test suite count 424,
  test file listing and aibox.py line count refreshed
2026-08-18 03:15:10 +00:00
qing db54921066 fix(aibox): batch I audit fixes - prompt write cap, light-subagent cancel propagation, atomic history writes
Remaining-item sweep (audit report §12):
- N9: cap prompt file writes (identity/soul/status, 10k chars) to bound
  persistent-injection surface
- F5: run_light_subagent switches to stream_chat + parent cancel_event
  (mid-stream interrupt); ExpressEmotionTool injects parent event
- Q4: busy_timeout=10s on mark_as_triggered (multi-process DB contention)
- Q5: malformed reminder_time tasks returned as due (were silently dropped
  by SQL-level string filter, which is now Python-side authoritative check)
- Q7b: load_conversation realpath prefix check (../ traversal defense)
- Q8: atomic JSON writes (tmp+os.replace) for index/current/archive
- F6: dedicated ws command executor for /new, /reload_prompts
- F9: --once hard-exit backstop after soft timeout
- PromptManager singleton lock; limit=0 semantics (None = no LIMIT)
2026-08-18 03:09:05 +00:00
qing d3acccd146 fix(aibox): batch H audit fixes - subagent cancel propagation, scheduler timeout, exec process-group reaping
Fourth audit round (see docs/reports/aibox_audit_20260817.md §11):
- H1: shared parent cancel event no longer cleared on subagent first think
  (N5 regression); parent-already-cancelled skips subagent start
- M1: /reload_prompts moved off event loop (run_in_executor, N3 sibling)
- M2: skill --help scan prewarmed before serving connections
- M3: scheduler task-level timeout (scheduler.task_timeout, default 600s)
- M4: get_recent_memories(None) capped instead of full-table load
- M5: memo delete now physically removes rows (was complete-only)
- M6: PromptManager.reload builds new list then atomically swaps
- L1-L12: read_file arg coercion, proxy save failure reported, SSE
  utf-8 tolerance, persona realpath check, truncation char boundary,
  compressor div0, skill path quoting, ask_user required, POSIX
  process-group kill (pgid captured at Popen), web scheme whitelist,
  registry redact substring match, Bearer redaction in failover logs
2026-08-18 02:33:17 +00:00
qing d1f780e381 feat(aibox): verify SOCKS5 connectivity before saving proxy config
- set_proxy 先做完整 SOCKS5 握手 + generate_204 探测(trust_env=False,超时 4s/6s),验证失败不落盘并按类型归因提示(auth/timeout/connect/missing_dependency/probe_failed)

- 异常类型优先分类,消息关键词兜底:Windows 本地化错误消息不影响归因

- 新增 skip_check 逃生口(代理未启动的预配置场景);禁用代理不验证;password_env 指向空环境变量时提前精准提示

- 测试:+9 用例(mock 网络 + 真实异常类型),全量 383 passed 零回归
2026-08-18 07:44:17 +08:00
qing cccc946660 fix(aibox): batch G audit fixes - llm hardening, thread lifecycle, cancel propagation, db transactions
P-series (llm_client): truncate non-transient HTTP error body to 200 chars, cap Retry-After at 3600s, validate fallback profiles (missing model skipped with warning), dedupe fallback list, remove dead _cancelled flag, document FailoverClient concurrency contract.

N-series (aibox/registry): batch-level drain thread with try/finally reclamation (N2), subagent cancel propagation via shared parent event + watcher thread (N5), /new reset moved off event loop (N3), streaming forward bounded by TOOL_MAX_RESULT_LENGTH (N8), persona path boundary check (N12), broadcast except narrowed + discard (N13), per-connection task set registration (N14).

db/history: FTS5 quote escaping (N7), mark_as_triggered single BEGIN IMMEDIATE transaction (N10), LIMIT parameterization (L1), archive filename collision suffix (N16).

Tests: +20 cases in test_g_fixes.py, full suite 375 passed.
2026-08-18 07:21:29 +08:00
qing 0246943165 feat(aibox): failover observability, timeout budget and Retry-After (L4c)
Observability: FailoverClient.get_stats() exposes aggregate counters
(requests / failovers / cooldown_skips / exhausted, snapshot copy);
new on_failover(from_name, to_name, reason) callback fires on every
failover decision — to_name=None marks chain exhaustion — for future
admin-panel/frontend wiring, wrapped so callback exceptions never
affect the request path.

Timeout budget: profile field "failover_timeout_budget" caps the total
seconds spent walking the chain (default unlimited; values <= 0 are
rejected to None with a warning). Once elapsed, remaining profiles are
skipped — prevents worst-case N x timeout stacking on longer chains.
Exhaustion is counted once per call via a single end-of-loop increment
(an initial double count at the budget branch was caught by the new
budget test and folded into the unified counter).

Retry-After: TransientLLMError now carries retry_after parsed from the
numeric Retry-After response header (non-numeric values like HTTP-dates
are ignored); FailoverClient._mark_fail uses it to extend that
profile's cooldown beyond the default 60s, respecting server-side
throttling on 429s. _fail_at entries became (fail_time, cooldown)
tuples; three existing tests that poke the internal map were updated.

Config: create_llm_client passes profile.failover_timeout_budget into
the FailoverClient constructor.

Tests: new TestL4cObservabilityAndBudget (13 cases: stats on
success/exhaustion/cooldown-skip, snapshot copy, hook called with
chain-tail None, hook exception safety, budget stops chain via mocked
monotonic, invalid budget ignored, config field plumbing, default
no-budget regression, Retry-After parse/ignore/extend-cooldown). Full
suite 355 passed.
2026-08-18 00:52:39 +08:00
qing d53eba77f0 feat(aibox): stream_chat first-event failover + cancel-flag guard (L4b)
FailoverClient.stream_chat now walks the chain but only fails over
while no event has been produced yet — replay before the first
reasoning/content/tool_call event is invisible to the user. Once any
event is out, failures propagate directly (replay would duplicate
frontend output and corrupt tool-call accumulation).

DeepSeekClient.stream_chat classifies non-200 responses: 429/5xx raise
TransientLLMError (failover-eligible), auth/param 4xx keep raising
RuntimeError. Chain exhaustion re-raises the last exception so the
existing rollback/error handling in think_and_respond catches it; an
all-cooling chain raises RuntimeError.

Cancel-flag guard: LLMClient.cancel() sets _cancelled because the
RequestException raised by force-closing the connection is
indistinguishable from a real network fault. FailoverClient.cancel()
is now defined explicitly (overriding __getattr__ delegation),
broadcasts to every client in the chain and sets the wrapper flag;
stream_chat/chat_completion reset it per call and suppress failover
when it is set — otherwise interrupting would make the system hop to
another vendor and keep generating against the user's intent.

Tests: new TestL4bStreamFailover (12 cases: failover before first
event on TransientLLMError/ConnectionError, no failover after first
event, cancel never fails over on both paths, cancel broadcast, chain
exhaustion, all-cooling, cooldown skip, success isolation, transient/
auth status classification). Full suite 342 passed.
2026-08-18 00:46:23 +08:00
qing 3d7f5ca3cb feat(aibox): chat_completion cross-profile failover via fallback chain (L4a)
When a profile declares "fallback": ["kimi", "local"], create_llm_client
now returns a FailoverClient wrapper holding the ordered chain. For the
idempotent non-streaming path (chat_completion — conversation
compression, light subagents):

- Fail over on transient errors only: HTTP 429/5xx (now raised as
  TransientLLMError by DeepSeekClient) and connection errors/timeouts
  (previously swallowed into None by chat_completion's catch-all, now
  re-raised). Auth/param 4xx failures keep returning None with no
  failover — config problems stay loud, consistent with the H1/H2
  error-early philosophy.
- Failed profiles enter a 60s in-process cooldown and are skipped
  until it expires; chain exhaustion returns None per the existing
  failure contract.
- Config structure errors fail at construction (non-list fallback,
  unknown profile name, self-reference — H2-style). Environmental
  problems (e.g. fallback profile missing its API key) skip that
  fallback with a warning instead of blocking the primary.
- Unspecified attributes delegate to the primary client via
  __getattr__ (name/model/api_key/cancel), so DeepSeekChat and
  set_client keep working unchanged. stream_chat still delegates to
  the primary directly; first-token failover is L4b.

create_llm_client refactored: single-profile construction extracted
into _construct_profile_client, shared by the main path and the
fallback chain (flat-api path untouched).

Tests: new TestL4aChatCompletionFailover (15 cases: chain build,
validation, 429/5xx/connection failover, 401 no-failover, exhaustion,
cooldown skip/expiry, missing-key skip with warning, transient raise,
4xx None regression, stream delegation); full suite 330 passed.
2026-08-18 00:42:57 +08:00
qing d29bb5504a feat(aibox): L-batch profile polish (display name, prefix escape, temperature, reasoning aliases)
L1 display name: client.name now resolves to the profile's
display_name field, falling back to the profile key — the dialogue
context no longer labels Kimi/Qwen sessions "DeepSeek". The legacy
flat api section keeps "DeepSeek" (zero breakage).
_resolve_model_profile now returns (profile, model, profile_name).

L2 prefix escape: "model:NAME" forces bare-model semantics even when
a profile happens to be named NAME; "profile:NAME" forces profile
semantics and raises a clear error (listing available profiles) when
missing, instead of silently falling back to bare-name behavior.
Empty prefixes are rejected. Wired through --model / create_llm_client
/ subagent model params.

L3 profile temperature: new profile field "temperature" becomes the
client's default_temperature. chat_completion/stream_chat treat
temperature=None as "use profile default, else the old hardcoded
0.3/0.7", so explicit caller values still win. DeepSeekChat and
run_light_subagent resolve CLI > profile > constants.DEFAULT_TEMPERATURE.

L5 reasoning field aliases: _parse_delta recognizes
reasoning_content / reasoning / thinking / thinking_content (first
non-empty wins), so non-DeepSeek vendors' thinking streams are no
longer silently dropped.

L4 (cross-profile failover) intentionally deferred: streaming
failover needs request-replay semantics and is feature-level work.

Tests: new TestLBatch (16 cases); 7 existing unpack sites updated to
the 3-tuple. Full suite 315 passed.
2026-08-18 00:06:23 +08:00
qing a583f44d5d refactor(aibox): make profile reasoning_enabled a constructor param (M3)
Profile-level reasoning_enabled was attached as an instance attribute
after construction only when a profile matched; the base class never
declared it and consumers probed with double getattr. Any code that
accidentally set client.reasoning_enabled would silently override the
global switch.

The flag is now an explicit LLMClient constructor parameter with
three-state semantics: True/False (profile-level explicit, or the
global api.reasoning_enabled value concretized when the profile omits
the field) and None (no profile -> consumer falls back to global).
create_llm_client passes it at construction and no longer mutates the
client afterwards; DeepSeekChat._reasoning_enabled simplifies to a
single getattr probe that still tolerates clients without the attr.

Tests: TestM3ReasoningConstructor (4 cases) covering profile True,
profile-omitted -> global concretized, base default None, and the
None -> global fallback at the consumer; one legacy assertion updated
to the new contract (attribute exists, value None). Full suite 299
passed.
2026-08-17 23:58:45 +08:00
qing 5f3ca9a246 fix(aibox): sync compressor and snapshots on client swap via set_client (M1)
run_full_subagent replaced sub_chat.client by direct assignment after
construction, leaving ConversationCompressor (bound to the original
client in DeepSeekChat.__init__) and the api_key/name snapshots
pointing at the default model — compression requests silently kept
using the default endpoint/model while the dialogue itself used the
requested one.

DeepSeekChat gains set_client(): swaps self.client and refreshes
api_key, name, and compressor.llm_client in one place. run_full_subagent
now routes model overrides through it.

Tests: new TestM1SetClient (2 cases: full reference refresh on a real
DeepSeekChat instance, and run_full_subagent calling set_client with
the new client); full suite 295 passed.
2026-08-17 23:48:40 +08:00
qing 77262f8c3a fix(aibox): inject available model profile names into create_subagent (M2)
The LLM had no way to know which profile names exist in the models
config section; a hallucinated name fell through to bare-name semantics
(default endpoint + bogus model) and failed at request time with API
400.

SubAgentTool now builds the model hint dynamically from config via
_model_names_hint(): description and the model parameter's JSON Schema
description both list the profile names, with the default profile
(api.default_model) marked. Empty or malformed models sections keep
the original wording untouched (zero breakage); exceptions degrade to
no hint.

README: tool table note, test count 293, new v3.4 changelog entry
summarizing the H1/H2/H3/M2 profile-hardening batch.

Tests: new TestM2SubAgentModelHint (5 cases); full suite 293 passed.
2026-08-17 23:42:04 +08:00
qing 84a3f6dd5e fix(aibox): make --model effective in WebSocket server mode (H3)
--model was accepted by parse_args but only consumed inside
handle_once_mode, so `python aibox.py --model kimi` (server mode)
silently ignored the override.

The model now flows as a process-wide override:
main() -> EnhancedAIManager(model=...) -> DeepSeekChat(model=...) in
all three AI creation paths: add_ai (main dialogue),
WebSocketHandler._create_session_ai (per-connection sessions), and
TaskScheduler._create_task_ai (scheduled tasks). Manager logs
"CLI 模型覆盖已启用" at startup for observability. --once mode
plumbing is unchanged; --model=None keeps config-driven behavior.

Tests: new TestH3ModelPlumbing (6 cases) mocking DeepSeekChat to
assert the constructor kwarg across manager/session/task paths plus
parse_args; full suite 288 passed.
2026-08-17 23:37:59 +08:00
qing 0e9efe4895 fix(aibox): validate model profile schema at config source (H2)
Profiles previously had no validation, so config errors surfaced far
from their origin and cryptically:
- explicit profile missing `model` fell back to the profile NAME as the
  model (a profile named "local" would send "model": "local")
- default profile missing `model` with no model param produced
  "model": null -> API 400
- missing `api_url` surfaced as requests InvalidURL

_resolve_model_profile now runs _validate_profile (dict type + api_url)
before reading fields and _require_model on the effective model,
raising ValueErrors that name the profile, the missing field, and the
config.json path to fix. Bare model names still legally substitute for
a default profile's missing model field.

Tests: new TestH2SchemaValidation (6 cases: missing model/api_url,
null-model via default profile, bare-name substitution, non-dict
profile, e2e create_llm_client); full suite 282 passed.
2026-08-17 23:31:06 +08:00
qing 4480bce684 fix(aibox): never fall back to deepseek.key for model profile endpoints (H1)
~/.cert/deepseek.key is a DeepSeek-only credential; when a models profile
specified its own api_key_env (e.g. MOONSHOT_API_KEY) but the env var was
unset, resolve_api_key silently read that file and sent the DeepSeek key
as Bearer to a third-party endpoint (cross-vendor credential leak).

resolve_api_key gains allow_key_file (default True for the legacy flat
api section); create_llm_client passes allow_key_file=profile is None so
any profile path raises a clear RuntimeError instead, pointing users to
set the env var or use "" for local keyless endpoints.

Tests: new TestH1KeyIsolation (4 cases) with a mocked readable
deepseek.key proving the profile path errors out while the flat api
section still reads the file; full suite 276 passed.
2026-08-17 23:28:25 +08:00
qing 90f5544673 docs(aibox): changelog F2 (registry timeout cleanup + reader finalization)
Batch F bullet now split into F1/F2 entries; F2 covers unified
timeout-path cleanup of tool streaming attributes and reader-thread
finalization with the CPython stream-lock correction; test count 272.
2026-08-17 22:49:45 +08:00
qing 5bcf22f9a7 fix(aibox): registry timeout cleanup + reader thread finalization (F2/N6+N4)
N6: both streaming-forward timeout paths (deadline hit, join(5)
expiry) previously returned early and skipped the
tool.streaming_output / tool.output_queue cleanup, leaving the daemon
thread writing through the instance attribute — zombie output could
leak into the next session's queue once the attribute got reassigned.
Now unified through a timed_out flag with cleanup at the single exit;
stream_tool_timeout becomes an instance attribute (3600s default,
test-tunable).

N4: reader-thread finalization after timeout/grace break. First
implementation followed the audit suggestion (close stdout + join),
but PoC regression proved close() blocks: CPython stream objects hold
an internal lock, so closing while readline is blocked parks until
readline returns (PoC-1 regressed 3s -> 15.2s, exactly the
Start-Sleep duration). Corrected strategy: when reader already got
EOF, close stdout to release the pipe fd; when reader is still
blocked, only join(timeout=2) then give up (daemon-bounded, thread
ends naturally when the grandchild exits and EOF arrives), keeping
the main flow prompt. Audit report updated with this correction.

Tests: new tests/test_f2_fixes.py (5 cases), PoC regression 5/5
(PoC-1/4 back to ~5s), full suite 272 passed.
2026-08-17 22:47:37 +08:00
qing 6ef5d11d9d docs(aibox): sync README with Batch E/F security fixes
- feature list & tool table: set_proxy password semantics (memory-only,
  password_env for persistence)
- config example: fix stale flat proxy section to actual proxy.socks5
  structure with password_env; add plaintext-not-persisted note
- changelog: new v3.3 section covering Batch E (command exec protection
  chain) and Batch F (credential protection), test count 267
2026-08-17 22:41:24 +08:00
qing eb88a77238 fix(aibox): never persist SOCKS5 proxy password to disk (F1/N1, +N11)
- config.set_proxy gains password_env (aligned with api_key_env
  convention): config file persists only the env var name; plaintext
  password lives in memory for the current session only
- _save_config deep-copies and nulls proxy.socks5.password before
  json.dump, so every save path is sanitized (defense in depth)
- get_proxy resolves password as in-memory plaintext first, then via
  password_env from os.environ; downstream consumers (llm_client,
  web tool curl args) unchanged
- legacy migration on load: plaintext found on disk is kept in memory
  for the session, file is immediately rewritten sanitized
- SetProxyTool exposes password_env and reports password source
- registry debug logs mask password/api_key/token/secret/authorization
  kwargs (N11)

Tests: new tests/test_config.py (3 cases: no plaintext on save,
password_env resolution, legacy migration); full suite 267 passed.
Audit report updated with Batch F progress.
2026-08-17 22:37:30 +08:00
qing 177a9ac9a6 fix(aibox): close audit findings H1-H3/M1-M2 (command exec protection chain)
- H1: CommandLineTool.execute_streaming now purely delegates to injected
  executor; removes 150-line duplicate Popen loop that bypassed 1MB cap,
  cancel() and timeout protections
- H2: thread the blocking readline into a daemon reader thread; main loop
  polls queue with 0.1s ticks so timeout/1MB checks fire even for silent
  commands (verified: 3s timeout on 15s sleep, was 15s+ before)
- H3: stdin=DEVNULL on both Popen sites; stdin-waiting commands now hit
  EOF instead of deadlocking the tool thread forever
- M1: heredoc delimiter uniquification in write_file guidance (content
  line equal to AIBOX_EOF no longer truncates into shell injection)
- M2: 3600s total deadline on registry streaming forward loop

Verification: PoC regression 5/5 PASS (timeout/1MB/cancel/delegation),
full unit suite 264 passed. Audit report updated with Batch E results.
2026-08-17 17:28:41 +08:00
qing d03dbc7f91 feat(aibox): batch D multi-model profiles + docs
- models 配置节:多 OpenAI Chat Completions 兼容端点按档案名切换,默认空 dict 零破坏回退旧 api 节
- --model 双语义:档案名(命中 models 节完整采用)/ 裸模型名(同端点换模型)
- resolve_api_key 档案级 env 三级语义:未配置回退 api.api_key_env / 显式名称 / 空串免密(Ollama/vLLM)
- 档案级 reasoning_enabled 附加 client 属性,DeepSeekChat._reasoning_enabled 优先读取
- llm_client 增加 openai_compatible/openai provider 别名
- README 更新:aibox/README 六处(特性/配置/多模型档案专节/CLI 参数/更新日志 v3.2/测试计数 264)+ 根 README aibox 小节
- 单测 17 项新增(test_models_profile.py),全套 264 项全过
2026-08-17 16:58:33 +08:00
qing a5af4bb404 feat(aibox): batch C once mode hardening
- SQLite WAL:subsystems/db.py 建库时 PRAGMA journal_mode=WAL,缓解多 CLI 进程并发写锁
- --worker-only CLI 参数:纯执行面模式(tool_profile=worker),注入精简 system prompt 避免人格/身份提示词污染
- --model / --temperature 覆盖参数:DeepSeekChat 构造透传,create_llm_client 与 stream_chat 两处生效
- 子代理工具失败透传:run_full_subagent(outcome) → SubAgentTool 打 _SUBAGENT_TOOL_FAILED_MARKER → _handle_tool_calls 检测置 orchestrator stats.tool_failed 并剥离标记
- Batch C 单测 5 项:worker profile 工具集、model/temperature 构造、子代理失败标记透传与剥离
2026-08-17 16:32:22 +08:00
qing 9e0d39c912 feat(aibox): batch B once mode refinements
- parse_args 新增 --no-save-memory / --show-reasoning
- DeepSeekChat.__init__ 增加 enable_memory 参数,干净禁用 MemoryTool(注册层不加入 schema)
- 修复 _think_and_respond_impl 压缩提示 print 污染 stdout:改走 output_callback(line)
- handle_once_mode 接入新参数:reasoning 默认静默 + show_reasoning 显式开启 + quiet 最高优先级
- 退出码 1 覆盖 orchestrator 工具异常(stats.tool_failed);worker 子代理非零码透传留 Batch C
- 新增 tests/test_once_mode.py(10 项:enable_memory 开关/回调分流/退出码状态/压缩提示不污染 stdout)
2026-08-17 16:00:45 +08:00
qing ce3fb7fc74 feat(aibox): add CLI once mode (--once/--quiet/--once-timeout)
- parse_args 新增 --once/--quiet/--once-timeout 参数
- 新增 handle_once_mode:stdout chunk 流式、stderr 过程行、SIGINT 三层取消、超时 Timer
- 退出码:0 成功 / 2 API 失败 / 3 中断 / 4 超时;与 WebSocket 零回归
- 配套开发文档:docs/reports/aibox_cli_once_mode_dev_doc.md
- 附带修复:memdb/test_memdb.py 并发测试硬编码端口
2026-08-17 15:45:06 +08:00
qing 8a9a061662 fix(aibox): 子代理返回截断修复——收尾报告自生成替换外部摘要器 2026-08-15 14:27:27 +08:00
qing 7f21b65428 fix(aibox): FTS5 中文检索 + Token 压缩阈值生效——Compressor 抽离解耦
- db.py: FTS5 改用 trigram 分词(中文可检索),旧 unicode61 表自动迁移回灌;search_memories 增加 LIKE 兜底(%/_ 转义、MATCH 异常防护)
- conversation_compressor.py: should_compress 补齐 Token 阈值判断(原为死配置),Token 触发时按比例压缩早期消息
- ConversationCompressor 抽离至 subsystems,注入 llm_client 打破与 DeepSeekChat 循环引用;aibox.py 3517→3018 行
- 新增 21 项测试(中文检索/旧表迁移/LIKE 转义/token 估算/压缩流程/失败回退),全量 232 项通过
2026-08-15 09:51:59 +08:00
qing 85133868f3 refactor(aibox): 抽离ConversationHistory至subsystems(Wave2阶段1)
- 新增 subsystems/conversation_history.py: 迁移对话历史管理器(318行),MemoryDatabase用TYPE_CHECKING注解避免循环导入
- subsystems/__init__.py: 导出ConversationHistory,更新docstring含Wave2
- aibox.py: 移除ConversationHistory类定义(3706->3388行),仅保留import与类型注解
- 新增 tests/test_conversation_history.py: 41项单测,覆盖构造/当前对话/归档/历史加载/memory_db集成
- 测试基线: 171->212项全过(12.13s),无回归
2026-08-13 16:54:32 +08:00
qing d47ef6efb4 docs(aibox): 同步v3.0模块化文档,修复测试路径漂移
- README.md: 更新行数(8450->3706)、重写目录结构(含tools/subsystems/tests包)、补充包依赖方向与测试运行说明、新增v3.0更新日志
- test_basic_tools.py: 4个ReadFileTool测试改用tmp_path fixture,消除cwd依赖
- 新增 docs/reports/aibox_refactor_progress_20260814.md: 重构进度报告
2026-08-13 16:26:41 +08:00
qing 791fac0efd refactor(aibox): 抽离B批12个工具类至tools包,附58个单测
B批(tools/):迁移依赖子系统的12个工具类(CommandLineTool/SearchSkills/ReloadSkills/InstallSkill/CreateSkill/MemoryTool/MemoTool/UpdateIdentity/UpdateSoul/SaveMemoryAndEndConversation/SetProxy/GetProxy/AskUser),aibox.py减少~1739行

修复MemoryDatabase.get_recent_memories(limit=None)的SQL错误(既有bug,None传给LIMIT ?导致datatype mismatch)

171个单测全绿,循环依赖障碍消除:B批工具直接依赖subsystems而非反向依赖aibox.py
2026-08-13 16:09:31 +08:00
qing eadba81c26 refactor(aibox): 抽离工具类与子系统,附单测基线
A 批(tools/basic_tools.py):迁移 7 个无外部依赖工具类(FileToolBase/ReadFileTool/WriteFileTool/ListFilesTool/SimpleWebCrawlerTool/GetCurrentTimeTool/CalculatorTool)

Wave1(subsystems/):迁移 5 个叶子子系统(CommandExecutor/SubprocessCommandExecutor/PromptManager/MemoryDatabase/MemoDatabase/SkillManager),aibox.py 减少 ~1440 行

新增 tests/ 单测 113 个用例全绿,依赖方向维持 common -> subsystems -> tools -> aibox 单向
2026-08-13 15:48:55 +08:00
qing 0cfb3a7581 refactor(aibox): extract hardcoded prompts to template files
- Move all default prompt content from PromptManager._ensure_* methods into aibox/prompts/ template files
- Add 8 new template files: 00_IDENTITY, 100_STATUS, 1_memory_search, 2_memory_save, 5_memo_management, 8_identity_management, 9_language_instruction, 11_emotion_management
- Refactor _ensure_* methods to copy from template directory via _ensure_from_template()
- _ensure_skill_files now auto-discovers N_*.md files instead of hardcoded list
- Update README.md with complete file manifest
- Single source of truth: aibox/prompts/ is the only prompt template source
2026-08-13 08:19:53 +08:00
qing 62fab2bc2c feat(aibox): strengthen proactive ask_user and express_emotion guidance
- Rewrite ORCHESTRATOR_TURN_GUIDANCE: remove conservative '宁缺毋滥' principle,
  add 5 trigger scenarios each for ask_user and express_emotion
- Rewrite conversation start guidance: split into 3 steps with concrete examples
- Improve AskUserTool description: from passive 'encountered a problem' to
  proactive 'actively care about user, clarify needs, confirm intent'
- Improve ExpressEmotionTool description: add 'like caring for a friend' tone
  and 4 usage scenarios
- Update 99_SOUL.md: add behavior rules 9 (proactive questioning) and 10
  (emotional expression), expand workflow from 7 to 9 steps adding emotion
  interaction and follow-up questioning
2026-08-13 07:59:53 +08:00
qing e9463a28fb feat(aibox): 每轮临时注入互动风格引导(情绪化表达/适时询问用户)
编排者每轮请求在 enhanced_messages 末尾临时拼接互动风格提醒
(ORCHESTRATOR_TURN_GUIDANCE),引导用 express_emotion/ask_user
保持有温度的对话,并带适度约束;不写入对话历史,工具调用递归续跑
时跳过,worker 子代理不受影响。
2026-08-13 04:04:08 +08:00
qing f623294af6 fix(aibox): 拦截编排者误调执行面工具并清理旧版 run_command 引导
主代理未注册 run_command 等执行面工具,但内置默认提示词与
search_skills/create_skill/install_skill 文案仍引导其直接调用。
新增 WORKER_ONLY_TOOLS 拦截:编排者误调时返回纠正信息引导改用
create_subagent;同步 3/4/6/7 号默认提示词与默认 SOUL 为编排者口径。
2026-08-13 03:56:19 +08:00
qing 15b4fb5698 chore(aibox): 添加 wo-shi-zhu 幽默互动技能定义
用户创建的轻量技能:当用户说"我是猪"/"猪猪"/"哼哼"等时,用可爱猪猪风格幽默回应,
让对话更轻松。纯文本技能描述,无代码逻辑。
2026-08-13 03:39:45 +08:00
qing 45bc019bfc feat(aibox): 子代理输出转发开关 subagent_stream_output(默认开启)
将上一轮的「子代理实时输出可见化」固化为可配置开关,避免在生产环境刷屏或泄露中间过程。

- common/config.py: 默认配置新增 subagent_stream_output: true
- aibox.py run_full_subagent: 读取该开关;关闭时 _effective_output_callback 置 None,
  子代理在后台静默执行(主对话侧栏不显示其思考/工具调用/回答),开启时按原逻辑
  转发 line/chunk/reasoning。
- 仅影响主对话侧栏可见性,不影响子代理执行与最终返回摘要;老用户配置缺该键默认 True(兼容)。
2026-08-13 03:38:20 +08:00
qing c9eaf988b3 feat(aibox): 子代理实时输出可见化(转发 line/chunk 到主对话侧栏)
此前子代理(create_subagent / run_full_subagent)的输出回调只转发 reasoning_* 三类思考消息,
导致子代理真正的回答文本(chunk)与工具执行过程(line,含命令输出/工具调用)被丢弃,用户看不到。

改动:
1. run_full_subagent 的 sub_think_callback 白名单放开,新增转发 line / chunk;
2. SubAgentTool._subagent_output 处理 line(🔹 [子代理] 前缀)与 chunk;
3. chunk 文本先累积于 _subagent_chunk_buf,遇非 chunk 消息或 execute 结束时由
   _flush_subagent_answer 统一冲刷(🤖 [子代理回答]),避免侧栏逐字断行。
仅主对话侧可见,worker 执行子代理本身不受影响。
2026-08-13 03:36:32 +08:00
qing 23a10c37c7 feat(aibox): 对话开始阶段注入引导提示词(查阅记忆/询问用户/情绪化)
在 DeepSeekChat._init_system_prompts 末尾(连接初始化、/new、/reload_prompts 均会重跑)注入一段仅对主对话(编排者)生效的系统提示词,引导智能体在对话开始时主动:
1. 用 memory 工具查阅历史记忆、保持上下文连贯;
2. 主动关心/澄清意图,必要时用 ask_user 工具向用户提问;
3. 以情绪化方式开场,适时用 express_emotion 工具表达情绪。
worker 子代理不注入,避免执行面被人格/情绪干扰。
2026-08-13 03:29:37 +08:00
qing 518c8b2b90 feat(aibox): v2.9 编排者-执行者工具分层(tool_profile)
主对话仅保留记忆/情绪/调度/子代理派生等管理面工具,不再直接持有命令行;
子代理(worker)仅注册 7 个执行面工具,无记忆/人格/派生/提问能力,
修复子代理可改写主人格(update_soul)与递归派生的隐患。
附 v2.9 配套运行期提示词模板(aibox/prompts/)与实施方案文档。
2026-08-13 03:05:47 +08:00
qing 14db64f7a2 refactor(aibox): 工具基类 Tool/ToolRegistry 抽离至 tools 包
延续 common 包的拆分模式,形成 common <- tools <- aibox 单向依赖,
为后续工具子类逐步迁移铺路;aibox.py 瘦身约 230 行,工具子类与注册逻辑零改动。
2026-08-13 02:50:20 +08:00
qing 6db0cf017e fix(aibox): 补全 common/logger 缺失的 logging 导入并固化 common 包抽取
- common/logger.py 漏导 logging / RotatingFileHandler,导致 main() 在
  Logger.init_file_logging() 处抛 NameError;现已补回(抽取自 aibox.py 时
  未携带该 import)
- 固化 aibox 模块化重构成果:common/ 包(i18n/config/constants/logger
  + __init__ 重导出)已落地,aibox.py 删除内联 I18n 改从 common 导入
- requirements.txt 新增 PySocks 依赖
2026-08-12 09:41:33 +08:00
qing 7467e7bd42 docs: 整理文档——修复全部 file:// 失效链接、补齐缺失模块 README
- 将 7 个文档中的 mac 绝对路径链接(file:///Users/qingsong/...)转换为仓库相对路径,
  并修正错误目标:aibox 入口 main.py → aibox.py、pymsg C++ 实现 → libmsg/inc/us/Msg.hpp、
  bot4cj cpp/cnn → cuda
- 主 README 补 liblogc 基础库条目与 confs2 归档说明,安装顺序补 ca
- 新增 liblogc / libtry / confs2 / 3d / memdb README
- 同步 db2 脚本迁移(db/create_sqlite3.sql → db2/scripts/)导致的失效引用
2026-08-10 15:48:29 +00:00
qing 08df3929b2 feat(core): services.yaml 启动配置 + _run.sh 统一参数透传
Step 1: init.txt → services.yaml
- 新增 InitParser (yaml-cpp), 支持 YAML/TXT 双模式解析, 向后兼容 init.txt
- 新增 services.yaml 声明式服务配置 (name/cmd/args/depends_on/sleep_before)
- 改造 pmc_mtd.cpp 集成 InitParser, 支持目录/.yaml/.txt 三种路径模式
- Makefile 添加 InitParser 编译目标 + -lyaml-cpp 链接
- setup 脚本添加 libyaml-cpp-dev / yaml-cpp-devel 依赖
- core/_run.sh 改为 -i . (目录模式, 自动查找 services.yaml → init.txt)

Step 2: 11 个 _run.sh 统一参数透传
- 8 个脚本改动: 追加 "$@" / 移除 sleep / 修引号
- waf 保留 -c config.yaml 作为脚本默认值
- services.yaml waf args 改为 [] 避免重复传递
- 开发方案文档: docs/reports/core_services_yaml_dev_doc.md
2026-08-10 03:16:28 +08:00
qing 7ff73b7d07 fix(aibox): 缩短 WebSocket 心跳间隔并补全直连页面心跳
前置代理(Nginx/CDN)的 proxy_read_timeout 约 20s,原 25s 心跳
无法在超时前到达,导致连接被代理切断。aibox.js 心跳从 25s 缩短至
10s;frontend.html 直连页面此前完全无心跳,新增 10s 应用层心跳。
2026-08-06 09:16:34 +00:00
qing fc351f569a fix(aibox): 禁用 websockets 服务端 20s 协议层 ping 超时断连 (ping_interval=None) 2026-08-06 16:16:12 +08:00
qing d680dcd058 fix: 修复 WebSocket 长连接空闲断线问题
三处协同修复:
1. 前端 aibox.js 添加 25s 心跳(ping),防止 gateway 30s 空闲超时
2. gateway libhttp doWsRead 对 WS 代理连接跳过空闲超时
3. aibox.py _handle_tool_calls 启动 drain 线程实时排空输出队列,
   避免 SubAgent 等长时间工具执行期间前端收不到数据
2026-08-05 01:58:19 +08:00
qing 058eb11f5f docs(aibox): 更新 README 补充 express_emotion 工具与 /search 命令(移除过时说明、命令表新增 /search、工具表新增 express_emotion、消息类型表补充 task_list/task_completed、更新日志新增 v2.8) 2026-08-05 01:51:10 +08:00