Commit Graph
538 Commits
Author SHA1 Message Date
0b641a9403 fix(importer): convert Equation.3 OLE formulas via MTEF v3, surface degrade telemetry (#1411)
* fix(importer): convert Equation.3 OLE formulas via MTEF v3, surface degrade telemetry

Legacy courseware stores formulas as Equation 3.0 / MathType OLE objects
whose only renderable form inside the .pptx is a WMF preview picture.
The importer cannot rasterize WMF, so those formulas degraded to a
hardcoded 1x1 "transparent" placeholder — actually a 50%-alpha red
pixel that rendered as a pink block once stretched over the formula
frame, with no way for callers to notice the content loss.

- Convert `Equation.3` / `MathType` OLE objects to LaTeX: detect the
  progId, resolve the embedding, parse the OLE compound file (cfb) and
  convert its `Equation Native` MTEF v3 stream (new utils/mtef.ts) —
  fractions, radicals, scripts, fences, big operators with per-family
  limit variations, embellishments, and Symbol-font local character
  encodings. Slot order follows the rtf2latex2e reference
  implementation and real MathType streams ([main, lower, upper]), not
  the archived spec prose. Any failure falls back to the picture path.
- Fix the placeholder constant to a truly transparent pixel; keep
  recognizing the legacy red one (exported isPlaceholderDataUrl).
- Surface degradation instead of failing silently: optional
  ImportPptxOptions.onWarning receives machine-coded warnings
  (media-unconvertible / formula-fallback-image / formula-degraded /
  element-dropped) at every placeholder consumption point (image,
  background, shape/text pattern fills, math fallback); a throwing
  sink is isolated so telemetry can never fail an import. A formula
  whose fallback picture is also a placeholder keeps its plain text as
  a text element.

* fix(importer): address review — LSCRIPT base duplication, one-sided fences, Symbol table, depth caps

Review round on #1411 (thanks @wyuc — the fuzz safety-net validation and the
adversarial constructions found what the spec-conformance rounds could not):

- tmLSCRIPT: stop re-emitting a script slot as the base group (isotopes
  rendered as {}_{6}^{12}{12}C and the output doubled per nesting level —
  a 221-byte stream could OOM the worker); the base is the following
  sibling, so the template emits only {}_{sub}^{sup}, and a leading script
  no longer steals the previous atom via the trailing-script lookahead.
  tvLSUPER writers that emit a single slot now fall back to it.
- One-sided fences: render the missing side as a null delimiter
  (\left. / \right.) instead of an unmatched \left — piecewise-function
  braces (tmBRACE var 1) produced KaTeX-invalid output that silently
  degraded to flat text.
- SYMBOL_FONT_LATEX corrected against URW StandardSymbolsPS AFM + Adobe
  AGL: 0x3C/0x3E/0x5B/0x5D are literal < > [ ] (≤/≥ live at 0xA3/0xB3,
  now mapped, along with the rest of the Symbol operator block);
  0x22/0x24/0x5C are ∀/∃/∴; added Chi/vartheta/varsigma, the phi/varphi
  split (0x66/0x6A), and 0x5E = \perp. Unmapped font-local bytes >= 0xA0
  now flag degraded instead of passing silently.
- tmLIM: variation roles were inverted — spec + rtf2latex2e eqn.c say
  0 = upper limit, 1 = lower limit; single-limit writers keep their limit
  via the same slot fallback as the big operators.
- Hardening: MAX_DEPTH = 200 nesting cap and a 64 KiB LaTeX output cap,
  both throwing MtefParseError (deep bombs now fail loud instead of
  RangeError/OOM); video poster joined the placeholder warning points.
- Tests: +6 — three REAL Equation Native stream fixtures (round-tripped
  from a legacy deck, pinning the font-local encoding and big-op slot
  order), tvLSUPER single-slot, tmLIM both roles, script-nesting perf
  guard; fence tests now assert KaTeX renderability instead of pinning
  broken strings. Suite: 85.
- Version 0.1.5 -> 0.2.0 (new public option/type/export + Math.degraded).
  Lockfile re-anchored on main with pnpm@10.28.0: only the cfb additions
  remain, no unrelated churn.

* fix(importer): address review round 2 — tmLIM function slot, full Symbol high-half, bra/ket, cap tests

- tmLIM: emit the main slot FIRST followed by the limits and inject no
  operator name (the reference `39.1 = limit: lower, #1 #2` puts the
  function in the main slot — hardcoding \lim duplicated it and glued to
  letter-leading main slots, crashing KaTeX with an undefined control
  sequence). An empty main slot falls back to \lim as a neutral base.
- SYMBOL_FONT_LATEX: completed the 0xA0–0xFF block from the URW AFM
  (~70 positions: ∫ ∑ ∏ ⟨⟩ ∂ ∇ ⇒ ⇔ ⋅ ′ ∅ ⊆ ⊇ ∈ ∉ ∪ ∩ …). Unmapped
  font-local codes now throw MtefParseError instead of passing through
  as Latin-1 (0xF7 was an integral extender rendering as ÷ — plausible
  but wrong math); radicalex (0x60) and C1 controls (0x80–0x9F) also
  throw, taking the picture fallback.
- tmDIRAC: var1 renders a bra `\left\langle L\right|`, var2 a ket
  `\left| R\right\rangle` per the reference (was wrapping both sides).
- Embellishment records now count against the record budget (a 10 MB
  embellishment-only stream no longer allocates 1 GB before the output
  cap fires).
- Tests: +6 — every cap now has its own assertion (output-length width
  case at 15k sibling CHARs, record-count at 20k, in addition to the
  existing depth test), previously-dangerous Symbol codes verified from
  the AFM, unmapped-code rejection, embellishment budget, tmDIRAC
  KaTeX-validity for all variations. Fixture claims corrected (no
  big-op selector in the three real streams; the reading is pinned by
  the spec-conformance tests). Suite: 91.

* test(importer): pin 0xD6/0xF3 Symbol glyphs through KaTeX, fix big-op test title

Follow-up to the round-3 review: the two glyph fixes landed without a
test, the BigOp test title still named the wrong slot order, and the
table comment claimed the high half was complete while unlisted
positions intentionally throw.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(importer): correct Symbol table comments — 0xF7 is parenrightex not an integral extender, high-half coverage is ~50 of ~70 positions

---------

Co-authored-by: Percy <percy@PercydeMacBook-Pro.local>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@openmaic/importer@0.2.0
2026-09-11 13:09:46 +02:00
puxiaoandwyuc d32c3f2a00 fix(importer): stop PPTX import from hanging in non-browser environments (#1424)
* fix(importer): stop PPTX import from hanging in non-browser environments

* format

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/importer@0.1.5 @openmaic/renderer@0.1.7
2026-09-11 12:53:27 +02:00
Yizuki_Ameandwyuc 33e7beb05e test(render-service): cover preview callbacks across tsx boundary (#1454)
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-11 12:44:42 +02:00
puxiao 2baf616843 fix(materials): sanitize owner id in byte-store key so uploads work on Windows (#1426) 2026-09-11 12:42:22 +02:00
caa6ef0edc fix(settings): read data.error for image/video provider test results (#1459)
The verify routes return errors via apiError(), which serializes
{ success: false, errorCode, error } — but the image and video
settings panels read data.message, so every failed connectivity
test renders 'failed: undefined' and the real upstream error
(auth failure, model not open, bad base URL, ...) is hidden.

All other settings panels (ASR, PDF, provider-config-panel,
model-edit-dialog) already read data.error correctly.

Fixes #1457

Co-authored-by: SeanLiew <seanliew@SeanLiewdeMacBook-Pro-2.local>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-11 12:37:38 +02:00
Percyandwyuc 6eb9492652 fix(classroom): adapt complete page to short viewports instead of clipping (#1461)
The complete page centers its content in an overflow-auto flex section.
When the content is taller than the stage viewport (common on laptops),
the centered overflow clips beyond the scroll origin at the top — the
trophy is unreachable and the page opens mid-content.

Restore an adaptive compact layout: a ResizeObserver with hysteresis
(760/820px) toggles the page between full and compact forms so the
content always fits, and a dedicated inner scroll layer keeps the
decorative background pinned to the viewport when scrolling is still
needed. Add an e2e spec covering both layouts and trophy reachability.

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-11 12:37:27 +02:00
wyucandClaude Fable 5.1 d5be393317 test(agent-runtime): make the material search budget test deterministic (#1453)
search_material stopped on whichever of the character budget or the
100 ms wall-clock budget fired first, but the budget test asserted the
exact character count, so it failed on slow CI runners when the time
budget won (observed scannedChars 786432 instead of 1000000).

Route the search path's clock through the injectable `now` dependency
the wait tool already uses, freeze the clock in the character-budget
test so only that budget can stop the scan, and add a time-budget test
that trips the deadline after exactly one chunk.

Closes #1452

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 05:33:42 +02:00
RiverandXingJia He 1fe07df8b9 fix(ai): let LLM calls outlive undici's 300s headers timeout (#1404)
* fix(ai): let LLM calls outlive undici's 300s headers timeout

A non-streaming completion only receives response headers once the whole
completion exists, so undici's default 300 s headers timeout bounds total
generation time. Thinking models (GLM-5.x at effort=max, MiniMax M3, …)
routinely exceed that on large prompts — the request then dies with
"Cannot connect to API: Headers Timeout Error" at exactly 300 s and, with
maxRetries 0, the scene never generates (observed on a self-hosted
deployment with glm-5.2 scene generation).

Route every outbound LLM request through an undici Agent with a
15-minute headers/body timeout, attached at the shared transportFetch
seam so the OpenAI-compatible, native OpenAI/Responses, Azure,
Anthropic-compatible and Google transports all inherit it (same
lazy-import dispatcher pattern the Google proxy transport already
uses). Native transports now always install the shared transport
instead of only when the server injects a validating fetch.

* fix(ai): harden the LLM transport timeout fallback

Review follow-ups for the undici headers-timeout fix:

- getLlmDispatcher(): drop the cached promise on rejection so one
  transient undici import/Agent failure can't brick every later call;
  transportFetch falls back to the base transport (no dispatcher) when
  the dispatcher can't be built, warning once per failure episode
- bedrock: install fetch: transportFetch like every other transport
- google proxy: build ProxyAgent with the same headers/body timeout
  budget (http/https proxies; undici's Socks5ProxyAgent drops these
  options for socks5://)
- transportFetch no longer overwrites a caller-supplied dispatcher
- tests: dispatcher failure fallback + retry, bedrock transport,
  caller-dispatcher preservation, google proxy budget

---------

Co-authored-by: XingJia He <hexingjia@mobirit.com>
2026-09-11 02:48:35 +02:00
LeoParkerOuandwyuc 28133f8a67 fix(renderer): lazy-load optional ECharts runtime (#1420)
* fix(renderer): lazy-load optional ECharts runtime

* test(chart): assert setOption is called after runtime is ready

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-10 23:30:46 +02:00
9fe650d994 fix(quiz): resolve AI answer keys to option values before grading (#1328)
* fix(quiz): resolve AI answer keys to option values before grading

AI-generated quiz answer keys are unreliable: the model may write the
correct answer as option CONTENT ("(6, 2)"), as a LETTER ("A"), or as a
formatting variant (full-width chars, extra spaces) of either. Grading
compares exact option values, so a learner picking the correct option
was still graded incorrect whenever the key was written as content or
in a different format.

- generation: normalizeQuizAnswer now resolves every answer variant to
  the matching option value (NFKC + whitespace-stripped + letter-prefix
  canonical matching against option values and labels); unknown answers
  pass through untouched
- grading: resolveAnswerKeyToValue retro-fixes already-generated courses
  whose stored keys hold content variants, mapping them to option values
  when exactly one option matches canonically; ambiguous keys are left
  alone instead of being silently re-pointed
- tests: letter/content/full-width/trailing-space/multi-answer/unknown
  cases + end-to-end grading with a content-variant key

* chore(generation): bump version to 0.3.2 for answer-key fix

The repo requires every publishable @openmaic package change to ship with
a version bump (CI check-package-version-bumps).

* style(generation): prettier-format package.json after version bump

* fix(quiz): use resolved answer representation in review renderers; prettier

- QuestionCard + quiz-view review modes highlight the option canonically
  matching the stored answer key (answerIncludesOption), so persisted
  content-variant keys highlight correctly after the grading fix
- prettier formatting on touched files

* test(quiz): guard possibly-undefined answer in multi-choice test

* fix(quiz): narrow answer-key matching to exact unique alignment; project review UI from the same resolver

Per review: matching policy narrows to the DSL option-identity contract.
- Generation: exact value or unique exact label alignment; unknown and
  ambiguous keys stay unresolved (fail closed). Prompt example unified to
  the DSL object-options + value-answer shape.
- Grading: persisted keys resolve via the same exact alignment; the
  resolver returns the option's actual value when exactly one option
  matches canonically-or-exactly; ambiguous keys stay untouched.
- Review UI: QuestionCard and both quiz-view review modes highlight via
  the same resolver projection (answerIncludesOption(q, optionValue)).
- Removed the broader equivalence rules (NFKC, whitespace stripping, case
  folding, wrapped/prefixed-letter probe) — semantic synonyms such as
  正交/垂直 remain uncovered, hence Refs #1326.

* fix(quiz): resolve answer keys for persisted keys only; align quiz template with the DSL

- grading: compatibility resolution (label -> value) now applies to the stored
  answer key only. A learner submission is compared as submitted, so an option
  label can no longer be accepted as a different option's value.
- quiz-content/user.md: replace the string-options/`correctAnswer` example with
  the DSL shape the system prompt defines ({ label, value } options and
  `answer` as an array of option VALUES), so generated keys are values.
- tests: regression that a label submission grades incorrect while the same
  question's value submission grades correct.

* test(generation): update scene-prompt golden snapshot for the DSL-shaped quiz template

The golden test pins the rendered user prompt for every scene kind; the
quiz-content template now emits the DSL shape, so the snapshot follows.

* fix(quiz): read label-stored correct answers in the editor's option rows

`quiz-edit-ops.toRows` decided correctness with raw value equality, so an
existing label-keyed quiz showed A/B as correct (the review UI and the grader
both resolve labels) while every row read as incorrect internally — and the
next option edit or reorder rebuilt `answer` from those rows and silently
dropped the key.

- `toRows` now uses the same resolved projection (`answerIncludesOption`).
- `normalizeQuizAnswer`'s docstring now describes the exact-only behavior it
  has had since the review: formatting variants are not normalized and
  ambiguous entries pass through untouched.
- Regressions: an unrelated option-label edit and an option reorder both keep
  a label-stored answer, and a multiple-choice toggle preserves it. All three
  fail against the previous raw comparison.

---------

Co-authored-by: CrazyPandaP <CrazyPandaP@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/generation@0.3.7
2026-09-10 23:28:20 +02:00
5873a1f657 fix(render): keep preview browser evaluation self-contained (#1421)
Co-authored-by: Bryan Nathan <bryan@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-10 23:12:09 +02:00
a99e2007cf feat(providers): add GLM-5.3 and GLM-5.3-Flash support (#1401)
Zhipu released GLM-5.3 (2026-08-19, text flagship) and GLM-5.3-Flash
(2026-08-26, native multimodal, 320B MoE / 18B active). Both serve a
1M-token context with 128K max output and always-on thinking controlled
by reasoning_effort low/high/max — the API rejects thinking
{"type":"disabled"} with code 1210 ("该模型始终思考,不支持关闭思考").

- catalog: glm-5.3 (text) and glm-5.3-flash (vision) entries, 1M/128K
- metadata: glm53Effort capability — effort-only, not toggleable,
  default effort max (same forced-thinking pattern as Fable 5)
- adapter: for non-toggleable GLM models, degrade a "disabled" thinking
  request to the lightest effort instead of sending the API-rejected
  {type:'disabled'}, mirroring the guard in the Anthropic adapter;
  this also un-breaks POST /api/verify-model, which hardcodes
  thinking off

Co-authored-by: XingJia He <hexingjia@mobirit.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-10 22:56:37 +02:00
Educator-AIEDandwyuc 6afa5b0af6 fix(ai): share thinking context across bundle evaluations (#1378)
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-10 21:50:54 +02:00
Itegel Borjiginandwyuc 7aef1bd49a fix: include sharp libvips in standalone tracing (#1407)
Two sharp versions resolve in the dependency tree (0.34.5 transitive ->
@img/sharp-libvips-* 1.2.4, 0.35.4 direct -> 1.3.3). sharp's native
libvips libraries are loaded via dlopen and are not statically analyzable,
so Next.js standalone output tracing omitted them; it traced the 1.2.4
libs while the runtime loaded sharp 0.35.4, which dlopens libvips 8.18.6.
The result was a boot failure on self-hosted Docker (Alpine/musl):

  ERR_DLOPEN_FAILED: Error loading shared library libvips-cpp.so.8.18.6:
  No such file or directory (needed by .../@img/sharp-linuxmusl-arm64/
  lib/sharp-linuxmusl-arm64-0.35.4.node)

This surfaced as "Agent runtime startup failed" and broke PDF/image
material extraction. The bug is platform-independent: it stems from the
standalone tracing dropping the wrong libvips version, so it affects any
self-hosted Docker deployment (arm64 or x64), not a specific architecture.

Force-include every @img/sharp-libvips-* native lib directory in
outputFileTracingIncludes so the libvips matching the loaded sharp binary
is always present in the standalone output, across platforms and versions.

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-10 21:41:07 +02:00
Mahesh Singh 57782f928f fix(tts): reject non-audio 200 responses before storing or billing (#1395) (#1405)
* fix(tts): reject non-audio 200 responses and resolve audioUrl envelopes (#1395)

* fix(tts): address review feedback on #1405 — drop follower, protect raw pcm, test billing invariant
2026-09-10 20:52:35 +02:00
wyucandClaude Fable 5.1 29735f10d0 fix(ssrf): keep cloud metadata endpoints blocked under ALLOW_LOCAL_NETWORKS (#1419)
* fix(ssrf): keep cloud metadata endpoints blocked under ALLOW_LOCAL_NETWORKS

ALLOW_LOCAL_NETWORKS=true exists so self-hosted deployments can point
providers at loopback, RFC1918 and split-horizon targets. It also
returned early from validateUrlForSSRF before any classification, so a
client-supplied base URL of 169.254.169.254 (or metadata.google.internal,
100.100.100.200, fd00:ec2::254) was accepted on such deployments.

Cloud instance-metadata endpoints are now rejected regardless of the
flag: literal hosts and IPv4-mapped forms are classified directly, and
non-IP hostnames are resolved so an answer that lands on a metadata
address is rejected too. DNS failure under the flag still fails open,
as before, because split-horizon DNS is an explicit use case of the flag.
Without the flag the DNS path now also rejects answers on metadata
addresses that are not RFC1918 (100.100.100.200).

The route tests that used the metadata address as their example of a
target the flag allows now use a private-network address instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GRaNc5E88r41GUWt2Y3uiQ

* fix(ssrf): widen the metadata set and bound the flagged DNS lookup

Review follow-up. Add the AWS ECS task credential and EKS Pod Identity
endpoints (169.254.170.2, 169.254.170.23, fd00:ec2::23), the Azure
WireServer address (168.63.129.16) and the legacy OCI IMDS address
(192.0.0.192) to the blocked set; the last two are neither RFC1918 nor
link-local, so they were reachable even without the flag. Recognise the
metadata addresses when carried inside 6to4, Teredo, ISATAP and NAT64
literals. Bound the DNS lookup done under the flag to three seconds and
fail open on expiry, matching the existing fail-open on error. Say in
.env.example that the set is a fixed list and that DNS failure is
allowed through.

Use the same private-network fixture for the reject and allow cases of
the four route guard tests so a partial NODE_ENV re-gate of the guard
goes red again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GRaNc5E88r41GUWt2Y3uiQ

* fix(ssrf): share the tunnel decoder between the private and metadata checks

Review follow-up. assertSafeIp now classifies metadata addresses through
isCloudMetadataAddress, so an ISATAP identifier under a globally
routable prefix that carries 168.63.129.16, 192.0.0.192 or
100.100.100.200 is rejected on the strict-fetch path too. isPrivateIP
uses the same tunnelEmbeddedIPv4 helper instead of its own copies of the
6to4, Teredo and ISATAP decoders, which also gives it NAT64. Tests cover
the globally-routable ISATAP forms, the NAT64 private case, and the
false-positive direction (tunnel literals carrying public or RFC1918
addresses stay allowed under the flag).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GRaNc5E88r41GUWt2Y3uiQ

* fix(ssrf): reject metadata hostnames before the flag branch

Review follow-up. The metadata hostname and address check now runs
before the ALLOW_LOCAL_NETWORKS branch, so metadata.google.internal is
rejected by name in both flag states (previously only under the flag),
and metadata literals get the metadata message rather than the one that
suggests setting the flag. Pin the tunnel decoder boundaries in tests
(2001:db8 is not Teredo, 64:ff9b:1 is not NAT64, 2003 is not 6to4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GRaNc5E88r41GUWt2Y3uiQ

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 19:44:20 +02:00
wyucandClaude Fable 5.1 7d989490f8 docs(security): state the severity and CVE process for advisories (#1417)
Add a triage section that says how severity is assigned and that
severity objections are answered in the advisory thread before
publication, and a disclosure note that maintainers request the CVE
through GitHub at publication time and keep the published advisory
consistent with the CVE record.


Claude-Session: https://claude.ai/code/session_01GRaNc5E88r41GUWt2Y3uiQ

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 19:06:09 +02:00
98e75479ba fix(render): enforce chunk worker cap (#1359)
Co-authored-by: Bryan Nathan <bryan@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-08 18:45:18 +02:00
wyuc 64621adfa0 feat(render-service): emit render and preview lifecycle events (#1397)
* feat(render-service): emit render and preview lifecycle events

The service is silent apart from one startup banner, which leaves a deployment
able to see *that* rendering happens — CPU, memory — but unable to answer how
many renders succeeded, how long they took, or how long they waited for a slot.

Three things compound to make the outcome of a render unobservable from outside
the process:

- Job state lives in `JobStore`, in memory by default, so every record is gone
  on restart.
- A failed render is reported to the client as HTTP 200 whose *body* carries
  `status: 'failed'`, so any proxy, gateway or dashboard counting status codes
  sees a perfectly healthy service.
- Nothing between admission and completion writes a line anywhere.

This adds one JSON line per lifecycle transition on stdout, which every
container log pipeline already collects:

- `render_job_submitted` — with the queue and running depth at admission
- `render_job_started` — with `queueWaitMs`, the time the job spent queued
  behind other renders. Nothing else exposes this today, and it is the signal
  that distinguishes "renders are slow" from "renders are waiting".
- `render_job_finished` — with the outcome (succeeded / failed / cancelled),
  the total duration, and, for a failure, its machine-readable code
- `render_admission_rejected` — with the reason a 429 was returned, on both
  `/render` and `/preview`
- `preview_request` — status and duration for every response the synchronous
  preview route produces, emitted from middleware so 200, 413, 429, 504 and 500
  are all covered without touching the handler's return paths

Events carry only bounded, low-cardinality dimensions: an opaque job id, a
state, a duration, a reason code. No client identity, no file or directory
names, no scene content, and no raw error strings — a render works on
untrusted, model-authored input, and none of it belongs in an operational log.
A test asserts that a failure's free-text message, which can contain a scratch
path, never reaches the log while its fixed-vocabulary code does.

`RenderCoordinator` takes the sink through its existing options bag
(`onEvent`), defaulting to the stdout emitter, so tests assert on events
without going through the console.

* fix(render-service): close the lifecycle for a job cancelled while queued

Cancelling a job that has not started yet takes a different path from
cancelling a running one: it updates the job store directly rather than going
through `finishNonSuccess`. So it emitted no `render_job_finished` event — a
submitted job was simply never heard from again, and any success rate computed
from the event stream would have been quietly wrong. It also left the job's
submission timestamp in the map, which is the one piece of per-job state these
events keep.

Both are fixed by closing the lifecycle in that branch too. The added test
asserts a queued-then-cancelled job reports a terminal event, and fails without
the fix.

* fix(render-service): make the lifecycle stream summable and its fields honest

An independent review of this branch found the event stream could not actually
be summed into the rates it exists to produce, plus two tests that could not
detect the regressions they were named for. All of these were demonstrated.

- A job could emit `render_job_finished` twice. The success event was emitted
  before the terminal `jobs.update`, so a rejected write landed in the catch and
  closed the same job again as `failed` — one render counted as both a success
  and a failure. `JobStore` is a seam a Redis-backed store can fill, so a
  rejected terminal write is a real case, not a hypothetical. The success event
  now follows the last fallible await, and `finishEvent` is idempotent: it keys
  off the timing entry it consumes, so any second close is a no-op.

- `durationMs` on a finished job was submission-to-finish while its doc claimed
  "time spent in the state the event closes", so p95 "render duration" tracked
  queue depth. It keeps that meaning, now documented, and the event also carries
  `queueWaitMs` and `renderMs` so a consumer can tell a slow render from a long
  queue without joining back to the started event.

- `running` counts jobs from the moment they leave the queue, but the execution
  permit is shared with `/preview`, so a counted job may still be blocked. The
  field is documented as "admitted to run" rather than claiming to be a count of
  renders in flight.

- `render_job_submitted` counted the submitting job in `queued`, so an idle
  service reported `queued: 1` and the obvious "alert when queued > 0" rule
  could never be written. It now excludes self, matching `render_job_started`.

- `render_admission_rejected` and `preview_request` called the module-level
  emitter directly, so an embedder injecting a sink silently received three of
  five event types. `AppDeps` now takes the same `onEvent` seam.

- The serializer walked `Object.entries(event)`. Excess-property checking made
  that safe for object literals but not for spreads, which are exempt — so the
  header's promise that no identity, path or content is ever logged rested on
  every future caller avoiding one. It now serializes an explicit field list.

Tests: the route tests passed an `executionGate` that `AppDeps` does not have,
silently discarded, and unnoticed because `tsconfig` includes only `src` so
typecheck never sees `test/`. Removed, and they now assert through the injected
sink instead of spying on the console. The leak test asserted only on event
shape and still passed with the cleanup deleted; it now observes the tracked
state through a `trackedJobs` accessor. Added regressions for the double close,
the timing split and the queue depth — each verified to fail without its fix.
2026-09-06 10:06:13 -07:00
wyucandwyuc f50a25644c release: bump to 1.0.1 and add the changelog entry (#1388)
Co-authored-by: wyuc <dhq1024@proton.me>
v1.0.1
2026-09-06 00:08:50 -07:00
wyucandwyuc 0bab621e09 harden input validation and outbound URL handling (#1387)
* harden input validation and outbound URL handling

- Validate the classroom id on the write path with the same allowlist the
  read path already applies, and assert in the storage layer that a resolved
  classroom file stays inside CLASSROOMS_DIR.
- Apply validateUrlForSSRF at every call site regardless of NODE_ENV; the
  guard already owns the ALLOW_LOCAL_NETWORKS escape hatch, so the extra
  environment condition only disabled the check outside production builds.
  Add a repository-scanning test so a gated call site cannot reappear.
- Re-validate every redirect hop of an outbound provider request through a
  shared transport, mirroring the per-hop loop proxy-media already uses.
- Restrict stored slide HTML to the formatting vocabulary the renderer
  produces, sanitizing at the classroom persistence boundary on both write
  and read, with a separate policy for KaTeX snapshots.
- Refuse the development persistence authenticator under NODE_ENV=production
  unless an explicit opt-in is set, and document it in .env.example.

* drop credential headers on a cross-origin redirect hop

The manual redirect loop reused the caller's init on every hop, so provider
credentials were re-sent to the redirect target even on a different origin.
The platform fetch drops Authorization itself when it follows redirects, so
mirror that: strip authorization, api-key, x-api-key and x-goog-api-key
before a cross-origin hop, matching case-insensitively and preserving the
shape of the caller's headers. Same-origin hops are untouched.

A streaming request body cannot be replayed onto the next hop, so fail with
a clear message rather than sending an empty body.

---------

Co-authored-by: wyuc <dhq1024@proton.me>
2026-09-05 23:56:37 -07:00
92d8b32a5a feat(deepseek): register deepseek-v4-flash-vision-exp with vision cap… (#1329)
* feat(deepseek): register deepseek-v4-flash-vision-exp with vision capability

DeepSeek's docs list deepseek-v4-flash-vision-exp as an official model
on the same OpenAI-compatible base URL (https://api.deepseek.com):
1M context, 384K max output, JSON output / tool calls / Responses API /
Anthropic API all supported, priced same as deepseek-v4-flash.

Register it in the deepseek catalog with vision: true so scene
generation can pass document images as vision content parts instead of
silently dropping them for this provider.

* fix(deepseek): register vision model in thinking metadata and update pinned lists

- add deepseek-v4-flash-vision-exp to THINKING_CAPABILITIES (deepseekEffort,
  same as v4-flash per official docs) so the model-metadata drift test passes
- update the pinned deepseekModels list in thinking-config.test.ts to include
  the new model

* docs(env): include deepseek-v4-flash-vision-exp in DEEPSEEK_MODELS example

* Revert "docs(env): include deepseek-v4-flash-vision-exp in DEEPSEEK_MODELS example"

This reverts commit 8040bac441.

* style(deepseek): apply prettier formatting

* test(deepseek): pin vision model capabilities; document vision-exp in env example

Review asks from #1329:
- assert deepseek-v4-flash-vision-exp capabilities (vision: true,
  streaming, tools) so a regression to vision: false fails the suite
- add the model to the DEEPSEEK_MODELS example in .env.example

---------

Co-authored-by: CrazyPandaP <CrazyPandaP@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-05 23:11:45 -07:00
Yunare Maia dde58944c1 chore(ci): migrate GitHub Actions off deprecated Node 20 runtime (#1343)
* chore(ci): migrate GitHub Actions off deprecated Node 20 runtime

- actions/checkout v4 → v5
- actions/setup-node v4 → v5
- pnpm/action-setup v4 → v5

Node 20 reached EOL; v4 actions still use node20. Verified via
driftcheck v0.1.8 (14 occurrences across 3 workflows, all node24).

* fix(ci): bump cache@v4→v6 and upload-artifact@v4→v7 in e2e job

Addresses review feedback from @YizukiAme on PR #1343.

- actions/cache@v4 → @v6 (lines 243, 314) — drop-in replacement
- actions/upload-artifact@v4 → @v7 (line 332) — Node 24 support landed in v6+,
  not v5; bump to v7 to ensure node20 runtime is truly eliminated.

The publish workflows (publish-packages.yml, publish-openmaic-skill.yml) remain
intentionally out of scope — tracked in #1345.

* chore(ci): trigger checks after rebase
2026-09-06 11:17:32 +08:00
ClayExcandwyuc 1e10f60b15 fix(generation): assign unique IDs per media request (#1368)
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/generation@0.3.6
2026-09-04 11:23:21 -04:00
Yizuki_Ame eb2cbc701a feat(pro): generate concise conversation titles (#1275)
* feat(storage): add automatic conversation title lifecycle

* feat(agent): generate concise conversation titles

* feat(agent): schedule automatic conversation titles

* fix(agent): harden automatic title lifecycle

* fix(agent): refine conversation title prompt

* fix(agent): harden automatic title boundaries
@openmaic/storage@0.29.0
2026-09-04 10:52:25 -04:00
puxiaoandwyuc b4ed3e90f3 fix(importer): pnpm install fails on Windows (#1372)
* update @openmaic/importer/rollup.config.js

* update @openmaic/importer/package.json

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/importer@0.1.4
2026-09-04 09:59:54 -04:00
Chip Li 4cfc7c17a6 fix(skill): bound generate_scene retries per page (#1370) 2026-09-04 08:50:26 -04:00
Yizuki_Ame 9104824b41 fix(slide): keep bottom table rows visible in classroom playback (#1366)
* fix(slide): keep table rows inside playback canvas

* fix(slide): preserve full-cell table alignment

* test(slide): focus table clipping regression

* test(slide): verify wrapped table geometry

* docs(slides): narrow table alignment contract
2026-09-04 06:04:25 -04:00
杨慎andClaude Code 3a0612e962 fix(server): bound skill zip inflation instead of trusting declared sizes (#1374)
parseUserSkillZip guarded the SKILL.md size against the zip central
directory's self-declared uncompressedSize and then handed the entry to
JSZip's async('string'), which inflates the real deflate stream no
matter what the header claims. A malicious zip declaring a tiny size
slipped past the guard, and with the upload route's 1 MiB archive cap
and deflate's ~1032:1 ratio, one request could expand ~1 GiB in the
server process — repeatable per request for memory-exhaustion DoS.

Inflate the entry's compressed bytes with node:zlib
inflateRawSync({ maxOutputLength: 70_000 }) so zlib aborts the moment
output crosses the cap, whatever the headers say. The declared-size
check stays as a fast path that refuses honest oversized zips without
inflating; STORE entries are byte-length checked directly since their
"compressed" content is the content. Over-limit output raises the same
UserSkillUploadError('The archive SKILL.md is too large.'), so callers,
route responses, and status codes are unchanged.

Tests: a forged-header bomb (a real zip whose central directory is
patched to declare a small uncompressedSize, with the forgery asserted
to have taken on reload) is refused; an honest oversized zip is refused
by the fast path; a forged STORE entry is refused by the byte-length
check; a STORE zip round-trips; a corrupt deflate stream still maps to
invalid-zip rather than a size refusal.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-04 02:23:09 -04:00
6334e9adde fix(deps): bump next, js-yaml, undici, nanoid, lodash, sharp for disclosed CVEs (#1357)
Advisory-driven dependency bumps for already-public HIGH CVEs.

- next 16.1.2 → 16.2.11 (GHSA-6gpp-xcg3-4w24, GHSA-89xv-2m56-2m9x, GHSA-m99w-x7hq-7vfj, GHSA-p9j2-gv94-2wf4 and related)
- js-yaml 4.1.1 → 4.3.0 (GHSA-52cp-r559-cp3m / CVE-2026-59869)
- undici 7.22.0 → 7.29.0 (GHSA-f269-vfmq-vjvj, GHSA-v9p9-hfj2-hcw8, GHSA-vrm6-8vpv-qv8q)
- nanoid 5.1.6 → 5.1.16 (GHSA-28wg-ghj8-5hjv)
- lodash 4.17.23 → 4.18.1 (GHSA-r5fr-rjxr-66jc)
- sharp 0.34.5 → 0.35.4 (GHSA-f88m-g3jw-g9cj)
- eslint-config-next aligned to 16.2.11

Detected by osv-scanner. Lockfile-only + package.json version pins; no application code changes.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-03 07:18:47 -04:00
wyucandClaude Code 0128212da4 feat(render-service): add POST /preview endpoint (fixes render_scene_preview 404) (#1285)
* feat(render-service): add PreviewGate admission control for previews

Independent admission gate (global in-flight cap + per-identity cap) so
synchronous previews never compete with the MP4 export queue. Release is
idempotent so every route exit path can call it safely.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(render-service): add single-page preview renderer

Renders a scene to a PNG via SlideCanvas + static markup using the existing
Chromium executable configuration, egress policy, and semaphore
infrastructure, with a deadline/AbortSignal.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(render-service): add preview config and resource pixel limits

Adds previewDeadlineMs / previewMaxInFlight / previewMaxPerUser config and
resource-profile viewport pixel limits for previews; declares the renderer
runtime dependencies installed with the render-service container.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(render-service): add POST /preview endpoint

Wires the preview endpoint: declared-size rejection, gate admission before
buffering, byte-capped payload parsing, scene/stage/viewport validation,
deadline/cancellation, PNG response, and 400/413/429/504/500 mapping. Fixes
the render_scene_preview tool's broken link (previously 404).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix preview rendering and deadlines

* fix(render-service): enforce preview deadline, memory bound, and shared Chromium limit

- Stalled preview uploads now observe the deadline signal, error the consumer,
  and cancel the reader; deadline aborts map to 504 and release the permit.
- Previews retain the extraction permit through rendering so parsed payloads
  cannot accumulate beyond the memory admission bound.
- RenderCoordinator owns a shared execution semaphore so previews and MP4
  renders respect the same global Chromium concurrency limit.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(render-service): harden preview admission and rendering

* docs(render-service): define the preview deployment contract

* fix(render-service): enforce self-contained previews

* fix(render-service): allow fragment CSS URLs in previews

* fix(render-service): allow background-only previews

* fix(render-service): control malformed preview rejections

* style(render-service): apply prettier formatting after rebase

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-03 07:00:27 -04:00
dd74cfe23b feat(web-search): add Exa provider (#1342)
Co-authored-by: white638 <212083322+white638@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-03 06:14:05 -04:00
宙见·星野andwyuc 9b7b42c4c4 fix: normalize GPT Image 2 generation sizes (#1346)
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-03 04:48:42 -04:00
Yizuki_Ameandwyuc e71b00c5c9 fix(agent): preserve generate_scene failure causes (#1316)
* fix(agent): preserve generate_scene failure causes

* test(agent): harden scene failure coverage

* test(agent): isolate warning log format

* test(agent): isolate warning log level

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/generation@0.3.5
2026-09-03 02:15:53 -04:00
Rupam Pal 132d01cd04 feat(canvas): add double-click text insertion (#1310)
* feat(canvas): add double-click text insertion

* fix(ci): format canvas double-click text insertion

* fix(canvas): prevent text insertion on double-click of locked elements

- Add guard to check if double-click event target is inside an editable element
- Prevents text box insertion when double-clicking locked elements (backgrounds/watermarks)
- Fixes event-propagation bug where locked element selection handler returns before stopPropagation()
- Add regression test suite for canvas double-click handler covering locked elements and blank canvas cases

Addresses review feedback from CxuPercy on PR #1310

* fix: remove unused import in canvas-double-click-handler test

* fix: improve test implementation to avoid DOM dependency

- Rewrite tests to use mock objects instead of actual DOM elements
- Properly type MockTarget interface to satisfy TypeScript checks
- All tests now pass locally without DOM environment requirement

* test(e2e): add end-to-end tests for canvas double-click text insertion

- Add test for double-clicking blank canvas area to create new text element
- Add test to verify double-clicking existing elements does NOT insert new text
- Tests actual editor interactions, not just mock targets
- Addresses CxuPercy's review feedback for proper regression test coverage

* chore: format code with prettier

* test(e2e): simplify double-click text insertion test

- Remove flaky second test that checks element visibility state
- Focus on core functionality: double-click on blank canvas creates new element
- Use more lenient assertion (toBeGreaterThan) to account for dynamic elements
- Increase wait time for slide editor to fully render (1000ms)

* fix(storage): increase test timeout for replace/release operations

The 'replace retires rather than revokes an issued snapshot; release revokes both'
test performs multiple async operations (put, resolve, replace, release) that can
exceed the default timeout. Increased timeout to 10 seconds to accommodate I/O operations.
2026-09-03 11:35:30 +08:00
f760f58a70 feat(render-service): machine-readable admission state (429 reasons + health accepting) (#1351)
* feat(render-service): expose machine-readable admission state

429 rejections carried only prose, so clients couldn't distinguish
deployment-wide backpressure from a personal limit, and /health gave
load balancers no way to shed traffic before probing with a render.

- RenderRejectedError now carries a reason code: queue_full (global
  RENDER_MAX_QUEUE cap) or per_identity_limit (RENDER_MAX_JOBS_PER_USER
  guard). The internal reservation invariant stays reason-less.
- POST /render serializes the reason on 429 bodies via spread-omission,
  so reason-less rejections don't emit a reason field at all.
- /health gains `accepting: boolean` (queue below the global cap).
  Deliberately aggregate-only: no occupancy counts and never
  per-identity data — identity keys are client IPs when
  TRUST_PROXY_HEADERS=true, so the map must not leave the process.
- README documents the 429 reason codes, the accepting flag and why it
  is boolean-only, and the shared-`direct`-identity model that makes the
  default Compose disable the per-user guard.

Refs #1348

Co-Authored-By: Claude Code <noreply@anthropic.com>

* refactor(render-service): extract 429 body serialization and shared test helpers

Review findings on the admission-observability change: the 429 body was
built identically in both catch sites, and the parked-executor /
bounded-poll-loop shapes were duplicated across the route and
coordinator admission tests. The spec review also flagged that
/health's negative space was only verified by inspection.

- rejectionBody(error) serializes an admission rejection once; both
  RenderRejectedError catch sites collapse back to one-line mappings.
- The /health inline comment now cites RenderCoordinator#accepting
  instead of restating its aggregate-only rationale.
- parkingExecutor() moves to test/support/fakes.ts beside
  succeedingExecutor; waitUntil() lands in test/support/async.ts and
  backs both waitForPoll (route) and the coordinator settle check, with
  descriptive timeout messages.
- A new route test pins /health to its exact key set (accepting, ok,
  resourceProfile, versions) and a boolean accepting, so any new field
  must be a deliberate, reviewed change.

Refs #1348

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-02 08:38:59 -04:00
PercyandPercy 3405e7e3bd docs(readme): sync with current code, multi-workbench skill support, v1.0 videos (#1334)
* docs(readme): sync with current code, multi-workbench skill support, v1.0 videos

- Add missing PostgreSQL persistence and MP4 video export sections to README-zh (EN already had them)
- Update project structure: @openmaic/* SDK packages (dsl/generation/renderer/importer/editor/storage), render-service, 26 API endpoint groups
- Add storage layer to key architecture; fix outdated endpoint count
- Document that the OpenMAIC Skill works beyond OpenClaw: Codex, DeepSeek, WorkBuddy and other agent workbenches (rename section + anchors)
- Replace intro videos with v1.0 versions (EN + ZH)

* docs(readme): replace unverified cp command with import description for other agent workbenches

---------

Co-authored-by: Percy <percy@PercydeMacBook-Pro.local>
2026-09-02 20:20:37 +08:00
mehmet turacandwyuc 0352ffde4d fix(skills): normalize paths to POSIX before passing to pi-agent-core loader (#1297)
* fix(skills): normalize paths to POSIX before passing to pi-agent-core loader

On Windows, NodeExecutionEnv returns backslash-separated paths from
fileInfo() and listDir(). The pi-agent-core skill loader passes these to
the `ignore` package, which requires POSIX-style relative paths and
rejects absolute or backslash paths with:

  RangeError: path should be a `path.relative()`d string

Wrap NodeExecutionEnv with PosixNormalizingEnv that converts all
FileInfo.path values to forward slashes, and normalize skillsDir itself
before it reaches the loader.

Fixes #1295

* fix(skills): normalize FileInfo.name alongside path for Windows skill discovery

Address CR from HaningZS: on Windows, pi-agent-core's fileInfoFromStats()
returns the full backslash path as name, so entry.name === 'SKILL.md' never
matches. normalizeSkillFileInfo now derives name from the normalized path.

Add deterministic tests for normalizeSkillFileInfo and a production-boundary
test asserting listSkills() returns built-in skills.

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-02 06:01:18 -04:00
Zhenghan Songandwyuc ca3d785221 build(node): enforce direct dependency engine floor (#1337)
Why:
- The root Node 20.9 contract predates direct runtime dependencies whose declared minimums now reach Node 22.19.
- README, contributor, localized docs, and the OpenMAIC extension skill repeated the stale supported-version claim.

What:
- Raise the root Node minimum to 22.19 and align every operator, contributor, locale, and skill prerequisite.
- Add a check that compares the root minimum with installed direct production dependency engine minimums.
- Run the new contract check in CI after the frozen dependency install.

Risk:
- This changes the declared minimum only; no upper bound is added and Node 24 compatibility remains a separate concern.
- The localized docs build was validated with the independent #1306 boundary fix from PR #1307, which is not included here.

Tests:
- RED on the base: engine check reported pi-agent-core, pi-ai, svg-pathdata, and undici floors
- GREEN: root minimum 22.19 satisfies 35 engine-constrained direct dependencies
- Node 20 lockfile-only install reports the root unsupported-engine warning
- Node 22 frozen install and postinstall
- Docs build with PR #1307 boundary: 34 pages and all locale postexport checks; docs types:check
- Root Prettier, ESLint, TypeScript, i18n, package-version, and internal-dependency gates
- Root pnpm test: 7137 passed, 81 skipped

Live Docs:
- GitHub issue #1304 tracks the Node contract; #1306 / PR #1307 tracks the separate docs-build prerequisite.

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-02 04:29:52 -04:00
e58c700082 fix(generation): keep diagram node position off the hover transform (#1339)
* fix(generation): keep diagram node position off the hover transform

A CSS `transform` overrides the SVG `transform` presentation attribute, so a
generated `.node:hover { transform: ... }` rule wipes out the `translate()`
that positions the node and snaps it toward the origin while hovered. The
diagram prompt only asked to "avoid hover transform conflicts", which was too
vague to prevent this.

Spell out the contract instead: the outer `<g>` carries positioning only, and
hover/active animations apply to an inner group.

Refs #1300

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(generation): bump package version to 0.3.4

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/generation@0.3.4
2026-09-02 03:54:25 -04:00
Mark Tranandwyuc 55e324768e fix: keep videos playing inline on mobile (#1321)
* fix: keep videos playing inline on mobile

Add playsInline to every playback video path, including the legacy app renderer, renderer-backed playback, and the shared renderer package. This keeps mobile browsers such as WeChat on Android from interrupting inline playback and restarting videos after an interruption.

Add regression coverage for both app renderer modes and the package renderer.

* chore(renderer): bump package version

* chore(renderer): bump package version to 0.1.6

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/renderer@0.1.6
2026-09-02 02:36:47 -04:00
Yizuki_Ameandwyuc 3214cead2c fix(workbench): render LaTeX in assistant messages (#1309)
* fix(workbench): render LaTeX in assistant messages

* fix(workbench): harden streamed math rendering

* fix(workbench): preserve streamed math boundaries

* refactor(workbench): use standard math syntax

* fix(workbench): preserve math in course link labels

* fix(workbench): disambiguate single-dollar math

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-02 02:21:59 -04:00
Burak Keskinandwyuc 03bd449320 fix(settings): turn TTS and media generation on from the Settings page (#1311)
* fix(settings): turn TTS and media generation on from the Settings page

The global enable flags only auto-set on first provider sync, and Settings
had no switch for them, so a later API key never started narration or
image/video generation. Add the Settings toggles and turn each flag on
when that provider first becomes usable.

* fix(settings): route media enablement through isUsableMediaProvider

A force-disabled image or video provider could still flip the global flag
from a key paste, and keyless providers never did. Use the same usable
transition as TTS, including a baseUrl for lemonade and comfyui.

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-02 02:11:39 -04:00
Mahesh Singh c938f5e082 fix(asr): bypass AI SDK for custom providers to handle extra response… (#1283)
* fix(asr): bypass AI SDK for custom providers to handle extra response fields

Custom ASR providers (e.g. SiliconFlow) return OpenAI-compatible JSON but
include extra fields like segments, duration, and usage that the
Vercel AI SDK's strict Zod response schema does not expect, causing it to
throw an 'Invalid JSON response' error.

Previously, custom-asr-* providers were routed through
transcribeOpenAIWhisper() which delegates to the AI SDK's
experimental_transcribe. The SDK validates the raw HTTP response against a
strict schema, so any provider-specific extra field fails parsing even when
the 	ext value itself is perfectly valid.

Fix: introduce transcribeCustomOpenAICompatibleASR() which calls
/audio/transcriptions directly via raw fetch (the same approach already
used by funasr-asr and lemonade-asr). It extracts only data.text from
the response, making it tolerant of any extra fields a provider may return.
The official openai-whisper case is unchanged and still uses the SDK.

Fixes #1277

* style: fix prettier formatting in asr-providers.ts

* fix(asr): require model ID for custom providers, add regression test

Address review feedback from CXuPercy (#1283):

- The previous implementation silently omitted the model FormData field
  when config.modelId was falsy. Most OpenAI-compatible transcription
  endpoints treat model as required, so a custom provider configured
  without a model ID would go from working (via transcribeOpenAIWhisper's
  fallback) to silently sending a modelless request and getting a 400.

- Fix (Option A — fail fast): validate config.modelId before building
  the request and throw a descriptive error if it is missing, so the user
  gets a clear message rather than an opaque 400 from the upstream API.

- UI contract: the settings panel already shows an amber warning
  'Please add at least one model before using this provider' and the
  Add button is disabled when the model ID field is blank, so the
  UI is already aligned with the fail-fast behaviour.

- Update all 12 test cases to supply a modelId, and add a dedicated
  regression test 'throws when no model ID is configured' to prevent
  this from regressing silently again.
2026-09-02 02:00:42 -04:00
liangb612 d4ef5faa76 fix: replace rm -rf with cross-platform fs.rmSync in package scripts (#1338)
* fix: replace rm -rf with cross-platform fs.rmSync in package scripts

The build/clean scripts used the Unix \
m -rf dist\ command, which fails on Windows where cmd.exe has no rm. Replace with Node's built-in fs.rmSync, available on all platforms (Node >=20.9).

* chore: bump @openmaic package versions for cross-platform script fix
@openmaic/storage@0.28.1 @openmaic/dsl@0.11.1 @openmaic/importer@0.1.3 @openmaic/editor@0.0.5 @openmaic/renderer@0.1.5 @openmaic/generation@0.3.3
2026-09-01 21:39:10 -04:00
简律纯andwyuc 32bfd197eb fix(generation): load micropip before generated imports (#1282)
Generated Pyodide widgets can import micropip before it is loaded, aborting initialization. Add ordered prompt guidance, cover the packaged asset, and bump the generation package patch version.

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
@openmaic/generation@0.3.2
2026-09-01 04:33:17 -04:00
mehmet turacandwyuc a7e9dc8337 fix(providers): apply server-pinned models for image and video providers (#1298)
* fix(providers): apply server-pinned models for image and video providers

The server-providers API already exposes image models, and the server
resolves them correctly, but getServerVideoProviders() omitted the
models field entirely, and fetchServerProviders() on the client side
discarded the models payload for both image and video — it only set
isServerConfigured and serverDisabled.

This caused the client to fall back to built-in model IDs, which fail
on endpoints that require different IDs (e.g. Volcengine Ark Agent Plan
uses dotted aliases like doubao-seedream-5.0-lite).

Server: getServerVideoProviders() now exposes models, mirroring the
image listing. Client: fetchServerProviders() stores the server model
list as customModels with replaceBuiltInModels: true for both image and
video, matching the existing LLM provider model-filtering pattern.

Fixes #1251

* style(tests): wrap long assertion to satisfy prettier printWidth

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-01 04:22:49 -04:00
Zhengchao Anandwyuc 5cd3f75b98 fix(docs): swap English/Chinese user guide links in README badges (#1290)
* fix(docs): swap English/Chinese guide links in README badges

* fix(docs): swap English/Chinese guide links in README-zh badges

* fix(docs): swap English/Chinese user guide links only

Revert unrelated changes (apiKey, database URL) and keep only
the four guide-link swaps in README.md and README-zh.md.

Addresses review feedback from YizukiAme.

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-01 04:10:45 -04:00
Zhenghan Songandwyuc 96343c440e fix(docs): isolate standalone build from product middleware (#1307)
Why:
- The docs Turbopack project walked up to the repository root and compiled the product middleware.
- The v1.0 middleware's product-only feature-flag alias made every current docs build fail before static export.

What:
- Resolve the absolute packages/docs directory from the Next config URL.
- Set that directory as the standalone docs app's Turbopack root.

Risk:
- The setting affects only the docs Turbopack project; root product middleware and aliases are unchanged.
- Webpack-specific behavior is unchanged because the docs workflow builds with Turbopack.

Tests:
- packages/docs pnpm build: 34 static pages and all locale postexport checks passed
- packages/docs pnpm types:check
- Root Prettier, ESLint, TypeScript, and i18n gates
- Root pnpm test: 7137 passed, 81 skipped

Live Docs:
- GitHub issue #1306 tracks the regression and build evidence.

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-01 15:57:46 +08:00
6cb90a18b4 fix: enforce LF line endings for text files (#1296)
Co-authored-by: RRXXZZYY <RRXXZZYY@users.noreply.github.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-01 15:45:19 +08:00