Commit Graph

8811 Commits

Author SHA1 Message Date
GuoQing Zhang 1bf2a11981 Merge branch 'feat/2.6.0' of github.com:dataelement/bisheng into feat/2.6.0 2026-07-15 15:28:30 +08:00
GuoQing Zhang 055718be15 refactor: simplify reconciliation celery tasks 2026-07-15 14:56:23 +08:00
LineWalker 18748b23c0 fix(linsight): keep code-interpreter deliverables inside the harvested output dir
Task-mode results claimed a deliverable (e.g. a generated PDF) that never
appeared in the workspace panel. Root cause: the model wrote the file to an
ABSOLUTE path (/output/report.pdf, /scratch/*.png) which resolves to the
container filesystem root -- outside the per-task working dir the LocalExecutor
harvests. The file was never uploaded (file_list=[]) nor synced, so
get_final_result_file found no deliverable and the panel fell back to a
synthesized fallback report. The shared LocalExecutor cannot safely rescue
container-root files (cross-task leak), so the fix steers the model to relative
paths:

- Harden the code-interpreter tool description (local + e2b) to require the
  RELATIVE output/ (scratch/) dir and forbid absolute /output|/scratch, noting
  files outside the working dir are discarded.
- Add a deterministic, non-blocking corrective notice: when a run's code wrote
  to an absolute /output|/scratch path, append a system notice to the tool
  result so the model self-corrects on the next step
  (BaseExecutor.absolute_path_advisory + wiring in LocalExecutor.run).
- Tests: test/linsight/test_code_interpreter_output_path.py (18 cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:52:00 +08:00
GuoQing Zhang 7c04e52d2c fix(permission): avoid oversized grant user query 2026-07-15 14:35:44 +08:00
GuoQing Zhang 1ba990b2d0 Merge branch 'feat/2.6.0' of github.com:dataelement/bisheng into feat/2.6.0 2026-07-15 13:44:19 +08:00
GuoQing Zhang 3775045608 fix: deduplicate user access lookups per request 2026-07-15 13:40:19 +08:00
LineWalker f13be90401 fix(linsight): thread task-mode skill selection to the submit
Follow-up to the None≡[] skill gate: with skills now strictly opt-in, the
daily task-mode path (ChatView → useAiChat → unified /chat/completions →
_to_linsight_submit) exposed a latent gap — it never carried the picked
skills, so the stored SV had skills=None and NOTHING was materialized. The
picker looked broken ("workspace has no such skill file"). Previously this
was masked because None loaded every enabled skill.

Thread the selection end-to-end:
- APIChatCompletion gains `skills` (Track H); _to_linsight_submit maps it
  onto the linsight submit schema (None/[] = no skills, opt-in list = those).
- useAiChat sends the picked skill names (taskModeSkillsState('new')) on
  task-mode turns only; the daily chain ignores the field.
- tests: _to_linsight_submit forwards a selection / stays None when absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:51:34 +08:00
LineWalker 4e83ececae fix(linsight): recover ask_user questions crammed into a dict's question value
Live case (session b28d0dc6, model deepseek-v4-flash, input "skill test"):
the task-mode ask_user clarify card rendered a raw-JSON blob as its single
question title with zero options. Root cause is model-side — deepseek-v4-flash
did NOT escape the inner quotes in a question text (你想要的"skill test"是指什么?),
so its OpenAI-compatible function-call serializer corrupted the whole `questions`
array. The arg parser then produced a WELL-FORMED outer list/dict but crammed the
entire 3-question array into the FIRST dict's `question` VALUE (dropping the
opening `[{"question"` while keeping the `: "` separator). The trigger is
intermittent: it only fires when a question's text itself contains quotes (here
echoed from the literal user input) — which is why prior DeepSeek clarifications
without quotes rendered fine.

The existing recovery (854d45974) only re-parsed malformed STRING list-elements,
so this dict-value shape fell through unchanged. Extend the recovery to:
- re-expand a dict whose `question` value is itself a serialized questions array
  (gated by a quoted-JSON-key signature so ordinary prose is never mangled);
- reconstruct a blob that RETAINED the `: "` separator via `[{"question"` so the
  first question comes back clean (no leading `: "` noise);
- run the same crammed-array recovery on a malformed TOP-LEVEL string (still
  degrades to [] for arbitrary prose — reason-only park).

Frontend needs no change (one clarify tool_call per recovered question already
renders the multi-page card). 22 unit tests green, incl. the exact live blob as
a fixture and regression guards for prose/placeholder questions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:39:13 +08:00
LineWalker 68eb88e194 fix(linsight): treat absent skills selection as none, not all
Task mode was running skills the user never picked. Root cause: the submit
persisted `skills=NULL` (the field was absent — a stale/cached client or a
non-UI caller), and `materialize_session_skills` treated `None` as
"copy every governance-enabled skill" — the exact opposite of an empty
selection. Every enabled skill was then materialized into the workspace
`/skills/` subtree, advertised to the model, and executed, silently
defeating the picker.

- skill_provisioning: `None ≡ [] ≡ "no skills this run"` (guard on `not
  selected`); skills are strictly opt-in via an explicit non-empty list,
  still intersected with the tenant's governance-enabled set.
- useSubmitMessage (client): the daily-chat task-mode entry now sends an
  explicit `skills: []` instead of omitting the field, so the contract no
  longer depends on the downstream `|| []` guard.
- test + docstrings updated; `test_none_selection_copies_nothing` is a
  regression guard against the footgun.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:33:57 +08:00
dolphin 6c55be5f80 fix(client): eliminate TTS playback races with singleton sound + token guard
The Howl instance lived in a Recoil atom read through hook closures; with
the slow TTS-synthesis fetch in between, a conversation-switch race left a
stale reference: the old audio kept playing with no pause control, and two
sounds could play at once (the older one orphaned and unstoppable).

- Hold the Howl instance in a module-level singleton so pause/resume/stop
  always operate on the real current sound.
- Starting a new message's playback stops the current audio immediately,
  before the synthesis fetch, not after.
- Version every playback request with a token; a fetch resolving after a
  newer request started is discarded, and superseded Howl callbacks cannot
  clobber the new playback's state.
2026-07-14 22:58:32 +08:00
dolphin 88fe522008 fix(client): persist like state in appChat message cache on feedback
Workflow/assistant conversations keep messages in the chatsState cache and
do not refetch on conversation switch, so a like/dislike that only hit the
API lost its highlight when switching away and back. Write the new verdict
back to the cached message on click. Also correct ChatMessageType.liked
from boolean to number (0/1/2, mirrors chatmessage.liked).
2026-07-14 22:58:32 +08:00
GuoQing Zhang 5ea2f7362a fix(knowledge): restore space chat citations 2026-07-14 16:26:52 +08:00
GuoQing Zhang 52d6310cd1 fix(knowledge): preserve Milvus schema during rebuild 2026-07-14 15:01:08 +08:00
GuoQing Zhang 1803fb38ca fix: include tool identity in assistant onboarding errors 2026-07-13 20:07:40 +08:00
GuoQing Zhang 0aa82ba942 perf: optimize app square list queries 2026-07-13 16:51:11 +08:00
GuoQing Zhang 71232f8b66 fix: limit knowledge remark length 2026-07-13 15:51:52 +08:00
GuoQing Zhang c2e7ef4e68 Merge branch 'feat/2.6.0' of github.com:dataelement/bisheng into feat/2.6.0 2026-07-13 15:33:54 +08:00
GuoQing Zhang a1e374e5f9 fix(platform): guard invalid workflow theme config 2026-07-13 15:33:11 +08:00
dolphin 1142c065d6 fix(client): correct ServiceBusyNotice Button import casing (ui/button -> ui/Button)
The file is Button.tsx; the lowercase import resolved on case-insensitive macOS
but broke the case-sensitive Linux build (ENOENT on ui/button).
2026-07-13 12:04:51 +08:00
dolphin e6cb7a60a6 Merge branch 'feat/2.6.0-beta4' into feat/2.6.0 2026-07-13 11:56:32 +08:00
LineWalker 715ebaf991 fix(channel): sync information articles under per-tenant context
sync_information_article iterated channel_info_source (a tenant-aware table)
with no tenant context set, so on a multi-tenant deploy the first SELECT raised
NoTenantContextError and the daily article sync never ran — subscribed sources'
content never updated.

Mirror reconcile_all_tenants: enumerate active tenants (root + active children,
or the default tenant when multi-tenancy is off) and run the sync under each
tenant's context, isolating per-tenant failures. Add a sync counterpart
TenantDao.get_children_ids_active for the sync worker path, and fix a latent
missing `import asyncio` in the knowledge-space dispatch hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:41:36 +08:00
LineWalker c42a3edc08 fix(workbench): drop unused char-whitelist pattern on app-center text
Follow-up to 1208fa158: instead of only relaxing the pattern to allow the
empty string, remove it entirely to align with feat/2.6.0 (72fd1e8f0
"fix: unused pattern"). The whitelist could not stop XSS (it allowed <>/"'&)
yet rejected the empty default plus emoji / non-CJK text; real escaping is
done by the frontend (React text nodes, no dangerouslySetInnerHTML). Keeping
all three branches identical avoids a future merge conflict on these lines.

Update regression tests accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:31:51 +08:00
LineWalker 1208fa1580 fix(workbench): accept empty application-center text in WorkstationConfig
applicationCenterWelcomeMessage / applicationCenterDescription were declared
with default="" but a pattern requiring >=1 char (^[...]+$). Any tenant whose
stored workstation config left these fields empty or unset (the default) made
WorkstationConfig(**raw) raise a pydantic ValidationError, so GET
/api/v1/workstation/config returned 500 and the client rendered its full-screen
"system maintenance" overlay — /workspace/c/new appeared broken.

Relax both patterns from + to * so the empty string is accepted while keeping
the character whitelist for non-empty input. Add regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:02:36 +08:00
dolphin 32ee955eac fix(feedback): keep task-mode like highlight across conversation switch
The task result panel reads its liked verdict from the linsight store, which is
seeded once on hydration. An optimistic like updated only the button's local
state + backend, not the store — so switching away and back re-mounted the panel
with the stale store value and the highlight vanished (a full page reload fixed
it, since that re-hydrates the store from the server).

Sync the verdict back to the store on click via a new onLikedChange callback
(TaskTurnPanel + ExecutionFlow pass updateLinsight), so the store stays current
and the highlight survives switch-away/switch-back.
2026-07-11 11:13:02 +08:00
dolphin ae636b7a36 fix(feedback): task-mode like targets the real answer id, not the placeholder
In task mode the result panel's like/dislike showed as soon as the run hit
'completed', but the task turn still carried its streaming placeholder id
(userMessageId + '_') — the real persisted category="task" ChatMessage id only
arrived on a page reload. A like clicked the moment the panel appeared wrote to
a non-existent row and silently vanished (occasional, timing-dependent).

Source the feedback id from the linsight store instead of the conversation
message, and populate the store's real message_id at completion by reusing the
existing session-version-list enrichment (no new endpoint/field):
- Websocket final_result: fetch the version list, copy this version's real
  message_id + liked into the store
- TaskTurnPanel: ResultPanel reads linsight.message_id (history hydration
  already seeds it via the same endpoint); add allowFeedback prop for the
  share-view opt-out
- AiMessageBubble: pass allowFeedback instead of encoding it via messageId

The button now appears only once the real id is in the store, so show-time and
id-availability are aligned and the placeholder can never reach /liked.
2026-07-11 10:44:58 +08:00
LineWalker e40712bba9 fix(llm): preserve workbench models when a partial POST omits them
update_workbench_llm persisted the incoming WorkbenchModelConfig wholesale, so a
body without `models` (Pydantic defaults it to None) nulled the entire configured
dialogue-model list — the way a stale admin page wiped the Root config in prod.
Treat a missing/None `models` as "no change" and keep the stored list; an explicit
`models: []` still clears. asr/tts/chat_title keep their None-clears semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 20:49:27 +08:00
LineWalker ed0d4a0fca fix(llm): preserve workbench models when a partial POST omits them
update_workbench_llm persisted the incoming WorkbenchModelConfig wholesale, so a
body without `models` (Pydantic defaults it to None) nulled the entire configured
dialogue-model list — the way a stale admin page wiped the Root config in prod.
Treat a missing/None `models` as "no change" and keep the stored list; an explicit
`models: []` still clears. asr/tts/chat_title keep their None-clears semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 20:48:37 +08:00
LineWalker 432caf42df fix(model): guard empty-id workbench models from crashing the picker
A blank dialogue-model row ({id:''}) saved from the workbench model config
flows into the workspace model pickers as <SelectItem value="">, which Radix
forbids — it throws and crashes the whole /workspace/c/new page.

- client: drop empty/invalid-id options in AiModelSelect and the Linsight
  ModelSelector so a stale/blank model never renders an empty-string
  SelectItem value (defense that doesn't rely on backend sanitizing).
- platform: reject saving a blank model row in WorkbenchModel — validation
  only checked models.length, letting {id:''} through and overwriting the
  configured dialogue models with an empty row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 20:28:53 +08:00
LineWalker 13f1a28c27 fix(model): guard empty-id workbench models from crashing the picker
A blank dialogue-model row ({id:''}) saved from the workbench model config
flows into the workspace model pickers as <SelectItem value="">, which Radix
forbids — it throws and crashes the whole /workspace/c/new page.

- client: drop empty/invalid-id options in AiModelSelect and the Linsight
  ModelSelector so a stale/blank model never renders an empty-string
  SelectItem value (defense that doesn't rely on backend sanitizing).
- platform: reject saving a blank model row in WorkbenchModel — validation
  only checked models.length, letting {id:''} through and overwriting the
  configured dialogue models with an empty row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 20:26:49 +08:00
dolphin 3cbd4461d4 fix(channel): carry the real answer message_id on the stream end event
Channel article chat yielded the 'end' event BEFORE persisting the answer
ChatMessage, so the client never got the real id — the streamed answer kept its
temporary placeholder id and a like clicked before switching away wrote to a
non-existent row and vanished (same class of bug as the knowledge-space fix).

Persist the answer first, then emit the end event with its message_id so the
client can swap the placeholder out immediately (frontend useChannelChat already
consumes it).
2026-07-10 19:07:03 +08:00
GuoQing Zhang 72fd1e8f0f fix: unused pattern 2026-07-10 18:49:46 +08:00
dolphin 8964cbd134 fix(llm): TTS synthesis failure returns a business error, not HTTP 500
A TTS provider failure (e.g. empty audio) raised a bare exception that
propagated as HTTP 500, which the client's global interceptor treats as a
service outage and redirects to the maintenance overlay — a poor experience
for what is just 'this one utterance failed to synthesize'.

- backend: wrap the synthesis call, raise a dedicated TtsSynthesisFailedError
  (code 10026) instead of letting it bubble up as 500
- frontend: textToSpeech opts into the interceptor's translate-and-toast path
  (skip403Redirect) instead of the 500 escape hatch; TextToSpeechButton no
  longer masks the real error or double-toasts once the interceptor already
  showed the localized message
- add api_errors.10026 to zh-Hans/en/ja
2026-07-10 18:42:30 +08:00
dolphin e3d2ed803d fix(feedback): swap streamed-answer placeholder id for the real one on stream end
Knowledge-space chat (single-file / folder) rendered the streaming answer under
a temporary client-side placeholder id (userMessageId + '_') and never swapped
it for the real persisted ChatMessage id, so a like clicked before the first
page reload wrote to a row that never existed — it silently vanished. Reload
loaded the real id from history and worked fine, masking the bug as 'only
fails right after sending, fixed by refresh'.

- backend: the 'end' stream event now carries the persisted answer message_id
- frontend: useStreamChatSSE forwards it through onFinal; useFolderChat /
  useFileChat / useChannelChat swap their placeholder messageId for it so
  like/dislike (and any other per-message action) targets the right row
  immediately, not just after a reload
2026-07-10 18:42:30 +08:00
GuoQing Zhang 8925d75cfe Merge branch 'feat/2.6.0' of github.com:dataelement/bisheng into feat/2.6.0 2026-07-10 18:32:04 +08:00
GuoQing Zhang 76a6c79b51 feat: support MySQL TLS configuration 2026-07-10 18:27:21 +08:00
GuoQing Zhang b1c83c91a9 fix(platform): keep tooltips above select menus 2026-07-10 18:06:18 +08:00
GuoQing Zhang 43edbce50c fix(platform): cache resource permission checks 2026-07-10 17:20:10 +08:00
GuoQing Zhang 8d9958c221 fix(platform): stabilize workflow metadata filters 2026-07-10 17:19:50 +08:00
GuoQing Zhang 93b6d24780 fix: initialize quick-start knowledge workflow nodes 2026-07-10 16:40:42 +08:00
GuoQing Zhang 3075089e10 fix: bind knowledge file ids from query 2026-07-10 15:32:09 +08:00
GuoQing Zhang c407ceb12d fix(approval): settle withdrawn channel subscriptions 2026-07-10 13:47:03 +08:00
Kinyoo 71d7a3cbe2 style(client): tighten top padding to pt-4 across chat nav + knowledge headers
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:11:25 +08:00
Kinyoo 67c92b1b5a refactor(client): unified CommentDialog for feedback + menu-apply reason (UI unification infra)
Add a shared CommentDialog (optional-comment / reason dialog) + gallery FeedbackSection; migrate the message 点赞/点踩 feedback (dropping the bespoke MessageFeedbackForm) and the menu-unavailable apply-reason dialog onto it; feedback comment is now optional (com_feedback_placeholder, en/zh/ja).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:11:25 +08:00
dolphin ec99d17d43 feat(feedback): unify task-mode 点赞/点踩 on the task-result ChatMessage
The linsight task result is already persisted as a category="task" ChatMessage
in the unified daily conversation, so rate it through the shared chatmessage
feedback (/liked + /chat/comment) instead of a linsight_session_version-specific
column/endpoint. All four AI Q&A surfaces now share one storage + rollup path.

Backend:
- chat_helpers: agent history formatter returns liked + remark (fixes daily +
  in-conversation task-turn highlight on reload)
- workstation_schema: WorkstationMessage carries liked + remark (channel surface)
- linsight utils: get_task_feedback_by_version maps session_version -> its task
  ChatMessage {message_id, liked}
- linsight endpoint: session-version-list enriches each version with message_id +
  liked for the standalone linsight page

Frontend:
- ResultPanel: rate by messageId via likeChatApi / disLikeCommentApi
- TaskTurnPanel forwards messageId + liked; AiMessageBubble passes the task row's
  message.messageId (hidden on the read-only share view); ExecutionFlow reads them
  from the enriched session-version store field
- drop the now-unused likeLinsightVersion / commentLinsightVersion
2026-07-10 01:31:08 +08:00
LineWalker 18adc59068 docs(feedback): remove message 点赞/点踩 feedback PRD
The PRD doc is no longer needed; the 点赞/点踩 feature itself (98b48c93a) is
unaffected — only its now-redundant design doc is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:35:26 +08:00
LineWalker cefa784ea0 feat(client): calm neutral notice + manual retry for transient LLM errors
A model rate limit is the vendor's availability blip, not a BiSheng fault, so it
no longer renders as the red danger card. Transient/retryable errors (rate_limit /
network_timeout / service_unavailable) now show a calm neutral ServiceBusyNotice
(grey, role=status) with a one-tap Retry; terminal errors (quota exhausted / auth /
content filter / unknown) keep the red failure card -- red means act, grey means
just a hiccup.

- ServiceBusyNotice: shared neutral notice (optional title, detail disclosure,
  Retry button), styled after the existing task-terminated banner.
- Task mode: TaskErrorCard branches transient->notice and regains an onRetry prop,
  wired only on the /linsight ExecutionFlow via continueConversation (same SV).
  /c inline tasks get the calm visual but no button (re-send via the main input).
- Daily mode: thread the SSE error status_code through onError onto the message
  (ChatMessage.errorCode); AiMessageBubble renders the notice + Retry (reusing the
  already-wired regenerate) for transient codes {12046,429,503,10540,12045} and
  suppresses the copy/feedback toolbar on it.
- i18n: add com_error_retry (重试 / Retry / 再試行).

Pure frontend; backend classification from the prior commit already routes the
rate limit here. vite build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:13:31 +08:00
LineWalker 46074138ed fix(linsight): treat MaaS insufficient_quota (429) as retryable throttling
MaaS gateways (Aliyun Bailian / DashScope and other OpenAI-compatible
endpoints) reuse OpenAI's `insufficient_quota` code + "exceeded your
current quota" message for TPM/TPS throttling (429-Throttling.AllocationQuota),
which recovers on its own. The classifier treated it as billing exhaustion
(FAIL_FAST / quota_exhausted), so a transient rate limit surfaced as a scary
"quota used up / top up" failure instead of a friendly "service busy, retry".

Narrow _QUOTA_SIGNATURES to unambiguous money wording only (arrearage /
insufficient balance / 余额不足 / 欠费) and move the generic quota/额度/配额
family -- including insufficient_quota -- into _RATE_LIMIT_SIGNATURES. Quota is
still checked first, so a genuine arrears signal keeps winning over a co-occurring
throttle word. Net: insufficient_quota + 429 now auto-retries (middleware) and, if
exhausted, renders the friendly rate_limit copy in both task and daily chat mode;
genuine balance exhaustion still fails fast with the top-up guidance.

Update the classifier / resilience / workstation unit tests and add R4-R6 to the
mock-model E2E runner (retry-then-recover, exhausted->rate_limit, billing->fail-fast).
53 unit tests + 9 E2E checks green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:11:26 +08:00
GuoQing Zhang 871e830ca5 Merge branch 'feat/2.6.0-beta4' of github.com:dataelement/bisheng into feat/2.6.0-beta4 2026-07-09 20:57:36 +08:00
GuoQing Zhang 5c2802e9a4 fix: tool sse event twice 2026-07-09 20:57:27 +08:00
GuoQing Zhang 0b4b99bfd7 feat:remove auto add CITATION_PROMPT_RULES 2026-07-09 20:05:39 +08:00