Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 2026-11-02 — PR #2506 合并冲突修复

- 目标分支:`dataelement/bisheng:feat/bisheng-work-120@de60988d7b5fd9402dcb607fada97df12c9f1d57`;PR 原提交 `a13ee07a0c3544ff7eeee05026f1f5dbb8ba2080`,工作目录 `.worktrees/bisheng-dsh-user-id-search`。
- 冲突位于 `docs/STATUS.md` 同一插入位置的独立记录;已完整保留目标分支记录和用户 ID 搜索记录,业务代码自动合并。
- 本轮自动验证:隔离 Python 3.11 环境后端身份搜索、部门/模型授权与管理用户搜索 31 项通过;前端授权弹窗、部门树和模型授权 20 项通过;修改文件 ESLint、Ruff 检查与格式、多语言一致性、架构守卫和差异检查通过。
- 平台 530 个严格类型文件检查通过。GitHub Frontend Quality 在全量平台 lint 步骤报告 95 errors / 1 warning,并提示历史 suppression 待清理;本地复现相同数量,全部问题文件、lint 配置和 suppression 与目标基线一致。本次修改文件 ESLint 通过。远端检查记录:https://github.com/dataelement/bisheng/actions/runs/41366372552 。
- 交付范围为源码冲突修复与 PR 合并;真实企业部署、MySQL/DM8 执行、真实账号授权和浏览器端到端验收继续按既有计划执行。

## 2026-10-21 — 发送后清理技能草稿

- 根因:提交后的输入区重置覆盖文字和附件,技能仍保存在独立草稿状态,随后重新渲染到编辑器。发送流程现在先捕获任务技能参数,再统一清空本轮文字、附件和技能;发送前校验返回明确结果,校验失败保留草稿。
Expand Down Expand Up @@ -114,6 +122,14 @@
- 模型按钮独立拥有 13px/400 字体与左右 8px/4px 内边距,采用原生 14px 下拉图形,保留 Work 模型与操作区之间的活动槽留白;窄卡片按容器宽度收紧留白。首页已确认的标题与卡片布局保持。
- 定向 ESLint、生产构建、diff-check 通过;3006 已发布 037d2a0ed,459 个构建文件校验与健康检查通过,其他容器保持。Chrome 实测卡片 114px、28px + superellipse(1.5),模型 13px/400、左右内边距 8px/4px,下拉可打开并关闭。桌面 Chat 聚焦刷新后,和同窗口 Work 截图核对:发送箭头相对边界一致,禁用按钮底色均为 RGB(185,200,241),模型下拉与发送间距一致;标题与卡片位置保持。证据:主工作区 outputs/home-controls-20261020。

## 2026-10-20 — DSH 企业后台按用户 ID 搜索

- 基线:远端 `dataelement/bisheng:feat/bisheng-work-120@cbbe6e104b9373d2c25da1bc317d8dc429ef03bf`;工作目录 `.worktrees/bisheng-dsh-user-id-search`,修复分支 `codex/dsh-admin-user-id-search`。修改范围为毕昇企业后台,Desktop/Harness 沿用现有运行版本。
- 根因:模型授权人员查询只匹配用户名和内部数字编号,遗漏用户业务 ID `external_id`。查询复用毕昇主体搜索规则:用户名包含匹配、业务 ID 大小写敏感前缀匹配,保留内部数字编号精确匹配;关键字统一规范化。租户、账号状态、部门与游标条件继续在外层查询生效,列表沿用内部编号稳定排序。
- 授权弹窗提示更新为“搜索用户名或 ID”,同步中文、英文和日文。后台与前端接口结构沿用现有合同。
- 自动验证:原基线查询在新增回归中复现 4 项业务 ID 失败;修复后身份搜索、部门授权、模型授权与管理用户搜索共 31 项通过。授权前端 3 套件共 20 项通过,覆盖业务 ID 请求与人员结果呈现;平台 530 个及客户端 1457 个 strict 文件类型检查、变更文件 ESLint、Ruff、架构守卫、i18n 和 diff 检查通过。
- 平台全量 ESLint 为 95 errors / 1 warning,客户端为 22 errors / 1 warning,均与相同基线逐项比较完全一致;问题位于本次保持原样的文件。真实企业部署、MySQL/DM8 执行、真实账号添加授权与浏览器端到端验收待执行。

## 2026-10-20 — Chat / Work 首页布局对齐

- Work 参照为正在运行的 bisheng-work-chat-work@1f3bbb041、Harness 与 conversation UI 0.2.0-rc.2;远端 V1.2.0 当前 a77dd7612。此次改动集中在 Chat 网页首页。
Expand Down
5 changes: 4 additions & 1 deletion src/backend/bisheng/user/domain/repositories/dsh_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from sqlalchemy import inspect, or_
from sqlmodel import Session, col, select

from bisheng.common.subject_search import normalize_keyword
from bisheng.core.context.tenant import get_current_tenant_id, strict_tenant_filter
from bisheng.core.database.subject_search import subject_match_condition
from bisheng.database.models.department import Department, UserDepartment
from bisheng.database.models.tenant import Tenant, UserTenant
from bisheng.user.domain.models.user import User
Expand Down Expand Up @@ -53,8 +55,9 @@ def access_users(
col(UserDepartment.department_id).in_(department_ids)
)
statement = statement.where(col(User.user_id).in_(department_members))
keyword = normalize_keyword(keyword)
if keyword:
match = col(User.user_name).contains(keyword, autoescape=True)
match = subject_match_condition(User.user_id, User.user_name, User.external_id, keyword, name_prefix=False)
if keyword.isdecimal() and 0 < int(keyword) <= 9223372036854775807:
match = or_(match, User.user_id == int(keyword))
statement = statement.where(match)
Expand Down
58 changes: 58 additions & 0 deletions src/backend/test/dsh/test_user_identity_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Execute DSH identity search with tenant, department and cursor boundaries."""

import pytest
from sqlmodel import Session

from bisheng.dsh.domain.services.profile import profile_scope
from bisheng.user.domain.models.user import User
from bisheng.user.domain.repositories.dsh_profile import UserDshProfileRepository
from test.dsh.test_model_access import model_access_db # noqa: F401


@pytest.fixture
def identity_store(model_access_db): # noqa: F811
with Session(model_access_db) as session, session.begin():
for user_id in range(20, 27):
user = session.get(User, user_id)
user.source = f"test-{user_id}"
user.external_id = {
20: "EMP_001%",
21: "EMP_002",
26: "EMPX001-other",
}.get(user_id, "EMP_001%")
session.add(user)
return model_access_db


@pytest.mark.parametrize(
"keyword, expected",
[
("EMP_001%", [20]),
("EMP_00", [20, 21]),
(" \u3000EMP_001%\n", [20]),
("emp_001%", []),
("001%", []),
("first-login", [20]),
("20", [20]),
("%", [26]),
("missing-user", []),
],
)
def test_identity_and_name_matches(identity_store, keyword, expected):
with Session(identity_store) as session, profile_scope(2):
rows = UserDshProfileRepository.access_users(session, keyword=keyword)
assert [user_id for user_id, _ in rows] == expected


def test_identity_search_preserves_scope_and_cursor(identity_store):
with Session(identity_store) as session, profile_scope(2):
search = UserDshProfileRepository.access_users
assert search(session, keyword="EMP_00", department_ids=[31]) == [(20, "e2e-dsh-first-login")]
assert search(session, keyword="EMP_00", department_ids=[]) == []
assert search(session, keyword="EMP_00", unassigned_only=True) == [(21, "e2e-dsh-existing")]
assert search(session, keyword="EMP_00", user_ids=[21]) == [(21, "e2e-dsh-existing")]
first = search(session, keyword="EMP_00", limit=1)
assert [row[0] for row in first] == [20, 21]
assert search(session, keyword="EMP_00", limit=1, after_user_id=first[0][0]) == [first[1]]
with Session(identity_store) as session, profile_scope(3):
assert search(session, keyword="EMP_001%") == [(22, "foreign")]
2 changes: 1 addition & 1 deletion src/frontend/platform/public/locales/en-US/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2633,7 +2633,7 @@
"noWebSnapshot": "No web snapshot yet — check the stored text or open the source page."
},
"dsh": {
"searchUsername": "Search username",
"searchUserIdentity": "Search username or ID",
"departmentAndMember": "Department / member",
"configuredQuota": "Quota (Token/month)",
"configuredQuotaWan": "Quota (10k Token/month)",
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/platform/public/locales/ja/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2578,7 +2578,7 @@
"noWebSnapshot": "ウェブスナップショットがありません。登録テキストを確認するか、元のページを開いてください。"
},
"dsh": {
"searchUsername": "ユーザー名で検索",
"searchUserIdentity": "ユーザー名または ID で検索",
"departmentAndMember": "部門 / メンバー",
"configuredQuota": "設定枠(Token/月)",
"configuredQuotaWan": "設定枠(万 Token/月)",
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/platform/public/locales/zh-Hans/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2578,7 +2578,7 @@
"noWebSnapshot": "暂无网页快照,请查看入库文本或打开原网页。"
},
"dsh": {
"searchUsername": "搜索用户名",
"searchUserIdentity": "搜索用户名或 ID",
"departmentAndMember": "部门 / 成员",
"configuredQuota": "设置额度(Token/月)",
"configuredQuotaWan": "设置额度(万 Token/月)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ it('selects another department and fetches its direct members', async () => {
it('searches users across departments and shows their department names', async () => {
render(<ModelAccessDialog model={model} onClose={vi.fn()} />)
await screen.findByText('Alice')
fireEvent.change(screen.getByLabelText('dsh.searchUsername'), { target: { value: 'Alice' } })
fireEvent.change(screen.getByLabelText('dsh.searchUserIdentity'), { target: { value: 'Alice' } })
await waitFor(() => expect(getDshModelUserPermissions).toHaveBeenCalledWith(7, expect.objectContaining({ keyword: 'Alice', department_id: undefined }), expect.any(AbortSignal)))
expect(screen.getByRole('heading', { name: 'dsh.quotaSearchResults' })).toBeTruthy()
expect(await screen.findByText('Alice')).toBeTruthy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ export function ModelAccessDialog({ model, onClose }: { model: DshAccessModel |
<div className="flex shrink-0 justify-end gap-2">
<Input
boxClassName="w-72"
aria-label={t('dsh.searchUsername')}
placeholder={t('dsh.searchUsername')}
aria-label={t('dsh.searchUserIdentity')}
placeholder={t('dsh.searchUserIdentity')}
value={query}
disabled={saving}
onChange={(event) => setQuery(event.target.value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ beforeEach(() => {
vi.mocked(getDshOperation).mockRejectedValue(new Error('Pending'))
})
describe('model access layout', () => {
it('searches all departments by business ID and displays the matching user', async () => {
render(<ModelAccessDialog model={model} onClose={vi.fn()} />)
await screen.findByText('Engineering')
fireEvent.change(screen.getByRole('textbox', { name: 'dsh.searchUserIdentity' }), {
target: { value: ' EMP_001% ' },
})
await waitFor(() =>
expect(getDshModelUserPermissions).toHaveBeenLastCalledWith(
7,
expect.objectContaining({ keyword: 'EMP_001%', department_id: undefined, include_seats: true }),
expect.any(AbortSignal),
),
)
expect(await screen.findByText('User')).toBeTruthy()
})
it('expands roots with nested connectors, keeps descendants collapsed and permits root collapse', async () => {
render(<ModelAccessDialog model={model} onClose={vi.fn()} />)
await screen.findByText('Engineering')
Expand All @@ -113,7 +128,7 @@ describe('model access layout', () => {
)
fireEvent.click(root)
expect(screen.queryByText('Engineering')).toBeNull()
fireEvent.change(screen.getByRole('textbox', { name: 'dsh.searchUsername' }), {
fireEvent.change(screen.getByRole('textbox', { name: 'dsh.searchUserIdentity' }), {
target: { value: 'Platform' },
})
expect(screen.getByText('Organization')).toBeTruthy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,6 @@ describe('model-scoped authorization', () => {
await screen.findByRole('heading', { name: 'Root Organization' })
expect(screen.getByRole('dialog')).toHaveAccessibleName('qwen-max')
expect(screen.queryByRole('tab')).toBeNull()
expect(screen.getByRole('textbox', { name: 'dsh.searchUsername' })).toBeTruthy()
expect(screen.getByRole('textbox', { name: 'dsh.searchUserIdentity' })).toBeTruthy()
})
})
Loading