Commit Graph
103 Commits
Author SHA1 Message Date
tao.chen 6b7701852f refactor: style css -> tailwindcss 2026-08-24 18:34:43 +08:00
tao.chen 9ce0b106fa update: init tailwindcss 2026-08-24 17:34:55 +08:00
tao.chen 95b588b96c refactor: update artifact list style 2026-08-24 17:29:19 +08:00
tao.chen b8246b1e9b refactor: update artifact list style 2026-08-24 17:28:55 +08:00
tao.chen f09c1912fa chore: initialContent 2026-08-24 11:30:14 +08:00
tao.chenandClaude b493907775 feat(scripts): 跨 owner 懒加载目录树 + 跨用户可见 workspace/public
修两个后端接口问题:
1) /api/v1/workspace-directories 返回为空,目录树结构消失
2) 同 workspace 内脚本/数据互相可见但默认排除 private

后端改动
--------
* list_scripts / list_resources / list_workspace_directories 新增
  owner_user_id 可选 query 参数;缺省 = 当前请求者本人(scope 到
  workspace/{me}/...),传值时 scope 到该 owner 的子树。前端根加载
  默认只见自己一级,其他成员以折叠分组呈现。
* visibility 过滤统一:非 admin 请求者只返回 owner==me 或
  visibility ∈ {workspace, public};admin 跳过。owner=me 含自己
  的 private,owner=other 只剩其 workspace/public,排除他人 private。
* create_workspace_directory 两个分支 visibility 默认 'public'
  (非 private),使跨 owner 目录树可见;响应新增 owner_user_id 字段。
* platform.list_members 鉴权从 system_admin_context 放宽为
  系统管理员或该 workspace 活跃成员(让普通用户也能渲染同
  workspace 成员名册,用于跨 owner 分组)。
* main.py 注册 platform 模块(随 list_members 改动补齐导入)。
* .env.example 同步 common/config.py 26 个字段。

前端改动
--------
* ScriptExplorer.memberScriptGroups 改由 members 列表播种分组,
  display_name 取 members.display_name;inferredDirectories 现在按
  owner_user_id 标记,统一跨 owner 目录渲染。删除脚本目录页头与
  树分组标题的工作副本数量角标。
* WorkspaceTree 新增 ownerUserId 透传到 store.toggleExpanded;
  仅"我"的分组 mount 时 auto-expand,他人分组默认折叠,展开才
  调 loadOwnerGroup / owner-scoped loadScripts / loadChildren。
* scriptWorkspaceStore 引入 namespaced cache key
  (ownerCacheKey = `${ownerUserId ?? me}:${path}`),loadedScriptPaths
  / loadedChildPaths / loadedOwnerGroups 全部按 owner 隔离;
  toggleExpanded 用 loadPath === undefined 区分 group 头与真实
  目录,修"他人子目录点击不触发接口"的 loadPath 前缀误判 bug。
* api.ts / AuthContext 透传 ownerUserId 给 listScripts /
  listResources / listWorkspaceDirectories。

文档
----
* API.md: §3.2 创建目录 visibility 默认 public + 响应加 owner_user_id;
  §3.3.1 GET directories 加 owner_user_id 参数 + 响应字段;
  §3.4 GET scripts 改写为 owner 作用域 + visibility 过滤语义;
  §五.1 GET data-resources 新增,同一套统一语义;
  §7 intro 例外 — GET members 对系统管理员或 workspace 活跃成员开放。
* DEVELOP.md: Code layout 重写以反映 backend api/services/clients/
  schemas 拆分 + schedule domain/scheduling/application/execution/
  infrastructure 拆分 + common 子包(auth/storage/backends);
  Configuration 系统补全 26 个 settings 字段;新增
  "Owner-scoping + visibility (cross-owner browsing)" 小节;
  Per-service dev 注释用 uv run 的源布局要求;Add a new DAG endpoint /
  storage bucket 路径改为 backend/src/backend/api/* 与 services/*。

测试
----
* test_list_scripts_parent_path.py /
  test_resources.py 补充 owner_user_id 参数化直接调用 + LIKE
  前缀断言(workspace/{owner}/... 前缀)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 19:26:53 +08:00
tao.chen d68055a96c feat(scripts/resources): align list_scripts visibility with list_resources
69a9a48 把 data resources 的可见性从 list 恒真改成
workspace-wide + visibility 过滤 + admin 短路,但 scripts 端
没动。两端不对称,导致:

* 非 admin 调 list_scripts 走 user_relative_path →
  workspace/{当前用户ID}/...,永远拿不到别人的脚本
* count_scripts 同样 user-scoped,dashboard "全部脚本"
  只统计自己
* list_resources 响应没带 owner_display_name,data-only
  owner 的目录名回退到 userId.slice(-6),显示不友好

修复:
* list_scripts / count_scripts 改 workspace-wide(新建
  _build_list_scripts_workspace_descendant_prefix helper;
  旧 _build_list_scripts_descendant_prefix 保留标 deprecated
  避免破坏其它调用方);非 admin 追加
  or_(owner_user_id = me, visibility in {workspace, public})
  与 list_resources 完全对称;admin 短路
* resource_payload 加 owner_display_name 字段(与
  script_payload 对称);list_resources SELECT 加
  Users.display_name + outerjoin
* 前端 ScriptExplorer displayName 回退链:scripts 的
  owner_display_name → data resources 的 owner_display_name
  → 本人 user.display_name → userId.slice(-6) 占位
  ;ResourceItem 类型同步加 owner_display_name?: string|null

存储物理布局仍是 workspace/{user_id}/...,仅读取侧
listing/count 跨 owner。
2026-08-21 14:06:45 +08:00
tao.chen 48fe49df3b feat(scripts/data-resources): merge tree + add parent_path filter
- Backend: GET /api/v1/data-resources accepts parent_path; LIKE
  '{ws_id}/%/{escaped}/%' AND NOT LIKE '{ws_id}/%/{escaped}/%/%' on
  StorageObjects.object_key (workspace-wide, escapes _ and %, mirrors
  list_scripts parent_path semantics). 13 new tests in
  test_resources.py (helper unit / SQL compile / SQLite behavioral).
- Frontend: listResources gains parentPath arg, propagated through
  WorkspaceBoundApi + AuthContext binding. WorkspaceTreeGroup title
  count and ScriptExplorer header count now include dataResources.
  memberScriptGroups backfills data-only owners so users with only
  data resources still render a group. loadDataResources accepts an
  optional parentPath, default empty preserves prior behavior.
2026-08-21 12:36:43 +08:00
tao.chen a9b682c1a1 fix(scripts): sequence-token race + comment/import cleanup (Codex ffec234 follow-up)
Codex review of ffec234 flagged:

1. **MEDIUM — loadScriptCount in-flight race on workspace switch.**
   Previous implementation used `if (get().scriptCountLoading) return`
   to dedupe. That meant switching workspaces WHILE a fetch was in
   flight dropped the new fetch entirely; the stale response from the
   previous workspace then overwrote state, leaving the dashboard
   showing workspace A's total while the user is on workspace B.
   Fix: drop the dedupe-via-flag, use a module-level
   `_scriptCountSeq` counter. Every call increments, captures the
   seq at start, and the response/finally block only mutates state
   when `_scriptCountSeq === seq` — stale responses are silently
   dropped. Rapid workspace switches each get their own fetch; only
   the latest response wins.

2. **LOW — scriptCount scope comment was misleading.**
   "范围与 list_scripts(parent_path=\"\") 对齐" is wrong: the
   count includes all descendant depths, not just root-level.
   Behaviour is correct for the dashboard's "全部脚本" intent but
   the comment would mislead the next maintainer. Rewritten to
   explicitly call out that the count is the UNION across all
   parent_path depths, with the rationale for each design choice.

3. **LOW — local `from backend.scripts import _escape_like_pattern`
   inside `resources.py` keyword-search block.**
   The "avoid cycle" justification was false: scripts.py and
   resources.py don't import each other at module level. Moved to
   the top-of-file import block.

Skipped:
- get_workspace_tree `like_prefix` not run through the escape helper
  (user_id is a 26-char Crockford ULID so no `_`/`%` can appear, but
  the invariant is not documented at the call site). Pre-existing
  pattern; out of scope for this round.

Verified: pytest 65 passed; pnpm typecheck clean.
2026-08-21 11:31:07 +08:00
tao.chen ffec234e40 fix(scripts): actually escape LIKE pattern literals + scope count endpoint
Codex review of #36 + #37 surfaced that my prior `escape="\\"` only
declared the escape character — the pattern literals themselves still
contained unescaped `_` and `%`, so `parent_path="foo_bar"` continued
to match `fooXbar/...`, `foo2bar/...`, etc. My earlier ESCAPE-clause
assertions were tautological: they verified the SQL rendered the
ESCAPE keyword without ever checking that the pattern was actually
escaped. The tests passed; the leak persisted.

Fix in three layers:

1. Real escape: `backend/src/backend/scripts.py` gains
   `_escape_like_pattern(value)` that escapes `\` → `\\`, `%` → `\%`,
   `_` → `\_` (in that order — the escape char MUST be escaped first).
   `_build_list_scripts_descendant_prefix` now returns the escaped
   prefix. `list_workspace_directories` and `delete_workspace_directory`
   also escape their server-built prefixes. `count_scripts` escapes
   the user subtree prefix.

2. Same bug elsewhere: `backend/src/backend/resources.py:404` had the
   identical `DataResources.resource_name.like(f"%{keyword}%")`
   pattern; a search for "100%" would match everything. Now escaped
   too.

3. Count endpoint scope: `count_scripts` was workspace-wide and
   skipped the StorageObjects JOIN. Now INNER JOINs StorageObjects
   (drops orphans whose current_object_id is dangling) and filters
   by `workspace/{user_id}/` subtree so the result matches what
   `list_scripts(parent_path="")` would return. Multi-member
   workspaces no longer over-report, and orphan rows no longer
   inflate the count.

Frontend: `DashboardRoute` is not keyed by workspace/user (only
ScriptsPage is), so without a workspace_id dep the previous
workspace's count persisted across navigation. useEffect now depends
on `currentWorkspace?.workspace_id`; `loadScriptCount` clears the
count to null at the start of the fetch so the dashboard doesn't
flash a stale number.

Tests — backend/tests/test_list_scripts_parent_path.py
- Rewritten with three layers of coverage:
  * Pure helper tests for `_escape_like_pattern` (7 cases including
    backslash-escape-first ordering).
  * SQL-contract tests asserting the COMPILED PATTERN contains the
    escaped form (lowercased to neutralise SQLAlchemy keyword casing).
  * BEHAVIORAL tests on SQLite in-memory with the same LIKE
    semantics — proves the fix actually prevents the wildcard leak.
    Includes a negative test (without escape, siblings DO match) so
    the fixture is verified to exercise the bug.

Tests — backend/tests/test_count_scripts.py
- Updated to assert the JOIN + user-scope filter. New test verifies
  two different users in the same workspace get different subtrees.

Verified:
- pytest backend/tests: 65 passed (43 baseline + 12 list_scripts + 4 count + 6 helper/SQLite behavioral)
- pnpm typecheck: clean
- Raw SQL on MySQL (live DB) confirms `LIKE 'workspace/.../foo\_bar/%%' ESCAPE '\\'`.
2026-08-21 11:17:45 +08:00
tao.chen 79650c61ed feat(scripts): GET /api/v1/scripts/count + DashboardRoute wiring
After #34 the workspace store only holds the root-level scripts plus
whatever subfolders the user has expanded. DashboardRoute's
"全部脚本"/"工作副本" counts derived from scripts.length therefore
underreport the workspace total until the user navigates to /scripts
and expands every folder.

Fix: separate count endpoint + dedicated store field, mounted
independently.

Backend — backend/src/backend/scripts.py
- New endpoint GET /api/v1/scripts/count.
- Route declared BEFORE /api/v1/scripts/{script_id}/... so FastAPI's
  declaration-order matching does not interpret "count" as a script_id.
- Returns { data: { total: number }, meta: {} }; SQL is a single
  COUNT(*) on scripts filtered by workspace_id + status='active'.

Frontend — services/api.ts + context/AuthContext.tsx
- countScripts(workspaceId) client; WorkspaceBoundApi gains the field;
  AuthContext binding forwards workspaceId.

Frontend — state/scriptWorkspaceStore.ts
- scriptCount: number | null, scriptCountLoading: boolean.
- loadScriptCount() action: idempotent (no-op while in-flight), silent
  on failure (dashboard tolerates a stale count).
- Initial state and reset() clear both fields.

Frontend — features/platform/DashboardRoute.tsx
- Subscribes to scriptCount; calls loadScriptCount() on mount.
- Falls back to scripts.length until the count resolves so the
  dashboard never blanks.

Tests — backend/tests/test_count_scripts.py (new)
- 3 unit tests: scalar result handling, NULL coercion, route callable.

Verified: pytest 55 passed (52 + 3 new); pnpm typecheck clean.
2026-08-21 10:57:01 +08:00
tao.chen cee080b7df fix(scripts): AuthContext binding + load() cache invalidation (Codex review)
Codex (deepseek-v4-flash) review of feat(scripts) parent_path filter
surfaced four problems:

1. Critical — AuthContext listScripts binding silently dropped parentPath
   - frontend/app/context/AuthContext.tsx:224 had:
       listScripts: () => rawApi.listScripts(workspaceId)
     so loadScripts("foo/bar") hit GET /api/v1/scripts with no query and
     always received root-level scripts. Typecheck passed because
     `() => ...` is assignable to `(parentPath?: string) => ...`.
   - Fix: forward the parentPath argument.

2. High — load() cache invalidation gap on toolbar refresh / create / delete
   - load() replaced `scripts` with root-only items but never invalidated
     `loadedScriptPaths` for subfolders, so previously-expanded folders
     rendered empty (cached no-op on re-expand) and open tabs pointing
     into subfolders were dropped by the validIds filter.
   - Fix: load() now re-fetches every path currently in
     loadedScriptPaths (and loadedChildPaths for directories), then
     dedups by id/path. On initial mount the cache is empty so this
     degrades to a single root fetch.

3. Low — test docstring overclaimed coverage
   - The "actual ORM roundtrip is covered by the existing integration
     tests" line is false (no other list_scripts test exists).
   - Fix: honest docstring noting the repo has no endpoint integration
     test layer; SQL assertions are brittle to SQLAlchemy/dialect
     formatting.

4. Low — backend docstring overclaimed index role
   - Claimed `idx_storage_workspace_relative_path` "avoided全表扫", but
     the index isn't declared in the ORM model, only exists via the
     baseline migration's upgrade path, and EXPLAIN doesn't drive
     through it (scripts-first plan via idx_scripts_workspace).
   - Fix: accurate description — MySQL drives via idx_scripts_workspace,
     storage_objects PK lookup applies LIKE per row. Notes where to
     optimize if 10万-scale perf becomes a real problem.

Skipped findings (out of scope):
- Medium LIKE escape for `_` / `%` (pre-existing in
  list_workspace_directories; not introduced here).
- Low dashboard `scripts.length` count underreport (separate UI bug,
  pre-existing assumption that broke under the new semantics).

Verified: pytest 50 passed, pnpm typecheck clean.
2026-08-21 10:42:38 +08:00
tao.chen 02c25fcc8e feat(scripts): listScripts parent_path filter + lazy load by directory
Backend — backend/src/backend/scripts.py
- list_scripts accepts optional parent_path Query (default "").
- Extracts _build_list_scripts_descendant_prefix helper for the
  "direct children of parent_path" prefix.
- WHERE clause now adds:
    StorageObjects.relative_path LIKE '<prefix>/%'
    AND NOT LIKE '<prefix>/%/%'
  so deeper descendants and prefix-siblings (foo/bar vs foo/bar2)
  are excluded. Empty parent_path filters to root-level only —
  this is the symmetric, intent-aligned behavior the lazy-load
  frontend relies on.
- EXPLAIN confirms idx_scripts_workspace drives the scripts table;
  storage_objects PK lookup applies the LIKE filter per row.

Frontend — frontend/app/services/api.ts + scriptWorkspaceStore.ts
- listScripts accepts optional parentPath; built URL preserves
  the new filter param.
- Store gains loadedScriptPaths / loadingScriptPaths Sets and a
  loadScripts(parentPath) action: idempotent, in-flight dedupe,
  dedup-by-id when merging into the flat scripts array so existing
  find() callers keep working.
- load() now uses listScripts("") instead of bulk fetch — root
  only on first paint.
- toggleExpanded() triggers loadScripts(parentPath) in parallel
  with loadChildren(parentPath) so folder expansion loads both
  sub-directories and direct-child scripts.

Tests — backend/tests/test_list_scripts_parent_path.py
- 7 unit tests: prefix construction, normalization, traversal
  rejection, and SQL compilation contract (LIKE prefix + NOT LIKE
  prefix + scripts.status filter).

Verified:
- pytest backend/tests: 50 passed (43 existing + 7 new)
- pnpm typecheck: clean
- alembic: no schema changes (migration-less feature)
- EXPLAIN with real workspace: idx_scripts_workspace → PK lookup
2026-08-21 10:27:40 +08:00
tao.chen 3dc049f46e fix: optimize ArtifactListImpl 2026-08-20 20:29:22 +08:00
tao.chen 05c2157c4a fix: monaco-editor offline build error 2026-08-20 20:22:11 +08:00
tao.chen 3dfa055698 fix: monaco-editor offline build error 2026-08-20 20:16:03 +08:00
tao.chen 6a8bdf6b16 fix: monaco-editor offline 2026-08-20 20:11:06 +08:00
tao.chen d3259ba452 fix: monaco-editor offline 2026-08-20 19:30:23 +08:00
tao.chen 5c15e68a40 Merge branch 'feat/schedule-cleanup-handover' into develop 2026-08-20 19:27:32 +08:00
tao.chen e4d010ec76 fix: monaco-editor offline 2026-08-20 19:23:42 +08:00
Winnie bc62034241 feat: improve schedule cleanup and handover 2026-08-20 19:12:44 +08:00
tao.chen ae3b6d63e2 Merge remote-tracking branch 'aliyun/develop' into develop 2026-08-20 12:08:10 +08:00
tao.chen b600c6810b fix: P0-5 — upload-status rollback, streaming copy (LOCAL only), user re-verify, honest lock
B1: `_mark_upload_failed_and_raise` now commits on a separate session
  - Helper takes `request + upload_id`, opens a fresh session from
    `request.app.state.session_factory` and commits there before raising.
  - Closes the named-lock connection-pool leak Codex flagged: the old
    "commit-on-the-same-session" implementation could return the
    GET_LOCK connection to the pool before the enclosing
    `finally: release_named_lock` ran, leaking `mp:<hash>` for up to
    `pool_recycle` and re-opening the same-key upload race.
  - Same helper now used by `create_server_object_payload`'s put-failure
    branch — two failure paths have identical semantics.

B2: streaming copy for soft-delete + restore (`get_stream() + put()`)
  - LOCAL backend: zero-copy (aiofiles stream write). OOM fixed.
  - S3 backend: still OOMs on multi-GB objects — `put()` materializes
    the async iter via `b"".join(chunks)`. Multipart `put` is a
    follow-up; do NOT claim "OOM fixed on production" since production
    defaults to S3.

C1: worker re-verifies `Users.status='active' AND is_deleted=0`
  - `_assert_user_active` called from `_execution_context` after
    resolving `triggered_by`; skips `SYSTEM_CRON_USER_ID`.
  - `USER_DISABLED` error_code goes into the `NODE_FINISHED_EVENT`
    outbox payload — `schedule_node_runs` has no `error_code` column,
    the row only carries the `message` text. Docstrings corrected to
    say so explicitly (previous docstring falsely promised row-level
    observability).

F1: honest browser-local file lock
  - `api.ts` `acquireFileLock/heartbeatFileLock/releaseFileLock/
    releaseFileLockOnUnload` are now no-ops with comments stating they
    never call the network.
  - `scriptWorkspaceStore` dropped `tickHeartbeats`; `tickCleanup`
    simplified to just clear cache.
  - `useEditSessionLifecycle` dropped its 15s heartbeat `setInterval`.
  - `ScriptWorkspace.tsx` renders `.local-lock-banner` info bar when
    `isEditing`. Two tabs may still silently last-write — banner is the
    only guard (acceptable disclosure-only tradeoff).

Dead code: deleted the duplicate `upload_bytes_to_session` in
`backend/src/backend/storage_api.py`. The `services.storage` import
is now the only source of the function; `create_upload_record`'s
docstring updated to point at `backend.resources`.
2026-08-20 12:07:52 +08:00
tao.chen cc8ac220a5 perf(schedule): 减少重渲染与冗余动画
- useCanvasNodeDrag.moveDrag 改 ref 直改 DOM,松手才写 store,避免拖拽期间反复重渲染 SchedulePage
- runs 轮询改 setTimeout 链式 + 移除 runs 依赖,避免每 1.5s 重建 interval
- SchedulePage 拆 PositionDraftBadge / RunNowButton,删除冗余的 runs/runsLoading/positionDraftCount 订阅
- 拆 ScheduleEdge 组件 + React.memo + useMemo,切换 edge 选中时不再重算所有 edgePath
- 删除 schedule.css 中引用未定义 keyframes 的 modal-in/spin 死动画
- 删除 .schedule-edge-line transition、.schedule-run-result-backdrop backdrop-filter;补 .artifact-card transition

Refs: kaneo #19-#24
2026-08-20 11:27:45 +08:00
Winnie ff700d8db4 fix: stabilize cron scheduling and run history 2026-08-17 18:55:31 +08:00
xiaozhu 23823325e6 fix:writeText不可用问题 2026-08-17 17:31:12 +08:00
tao.chen 2e070fae78 fix: pre check 2026-08-14 19:13:17 +08:00
tao.chen 65d14b8610 update: dir tree 2026-08-14 15:02:55 +08:00
tao.chen 518853cbb0 fix 2026-08-14 12:40:36 +08:00
tao.chen 604e66ffe0 fix: DataResourceUploadModal.tsx 2026-08-14 12:28:22 +08:00
tao.chen 480e1cc638 update: data source 2026-08-14 11:59:06 +08:00
tao.chen 462617b66d feat: datasource 2026-08-13 20:13:37 +08:00
xiaozhu bcddaac083 fix:修复只读文件切换编辑文件时iframe重新加载问题 2026-08-13 17:24:55 +08:00
xiaozhu 9160175d51 update:只读文件隐藏操作按钮 2026-08-13 16:05:16 +08:00
tao.chen 532f0d6c91 fix: dir tree 2026-08-13 12:12:58 +08:00
tao.chen 83d0e0adca fix: bug
1. Cannot find name 'PythonEditorBuffer'
2. Cannot find name 'PythonEditor'
2026-08-13 11:26:31 +08:00
xiaozhu a2f515cf87 fix:锁定文件只读 2026-08-13 10:26:01 +08:00
tao.chen 302bda2ee9 fix: directory tree 2026-08-12 20:45:29 +08:00
tao.chen d600d5dbb1 fix: directory cached 2026-08-12 20:41:44 +08:00
tao.chen 8abe505d17 fix: multi owner 2026-08-12 18:58:18 +08:00
tao.chen c6c2481b96 feat: workspace 目录树服务端懒加载
- api.ts: listWorkspaceDirectories 支持 parent_path 查询; WorkspaceDirectory 增加 has_children; 同步 WorkspaceBoundApi 签名
- AuthContext: 绑定透传 parentPath
- scriptWorkspaceStore: 新增 expandedPaths/loadingChildrenPaths/loadedChildPaths, loadChildren/toggleExpanded, 局部刷新 createFolder/deleteDirectory
- WorkspaceTree: 移除 useState, 改为受控展开/加载状态
- ScriptExplorer: 从 store 读取并透传展开状态与 toggle
2026-08-12 18:06:35 +08:00
tao.chen a09551bd3c update: workspace members 403 2026-08-12 17:14:47 +08:00
tao.chen a86432b102 update: PythonEditor.tsx 2026-08-12 16:58:00 +08:00
xiaozhu e4b8c320a3 fix:css报错 2026-08-12 14:55:09 +08:00
xiaozhu fe65034aa8 feat:脚本只读 2026-08-12 14:34:31 +08:00
tao.chen dc4d190935 fix: python script 404 2026-08-12 13:45:15 +08:00
tao.chen 3981c32781 refactor: directory 2026-08-12 12:23:19 +08:00
tao.chen bff7b7f9a6 fix: bugs 2026-08-11 21:53:52 +08:00
tao.chen dd6f176ca8 fix: file lock 2026-08-11 21:26:55 +08:00
xiaozhu a8a54f1b78 fix:光标 2026-08-11 19:02:22 +08:00