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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

### 新增

- Wave 1(宿主原生组织):人才市场落到宿主 `org_chart.sqlite`(`talent_pool`)与 `/api/org-module/talent*`;资产总线 apply 无 Node 也写入名册/人才;治理待处理暂停出现在组织首页与工作台总览;`/organization` 即使桌面集成也不再被 iframe 吞掉(原 App 仅作过渡链接);目录叠加 `delivery`/`host_path`。双身份边界与默认零 Node 不变。迁移图:人才市场 `managed_node_iframe` → `org_ui_slice`(13→12 iframe,10→11 slice)。
- 双身份边界(P1.3):工作室本机登录与组织房间登录分成两扇门。组织管理员不再被抬成宿主 `Role.ADMIN`;工作室访客不能拿宿主通行证写组织房间业务。界面与文档用「工作室 / 另一间房间 / 各自留白」说明两条路径。
- P1.2:`freeos org export-standalone` 默认 `--mode full` 写出可商业化 openXYOS **源码包**(`openxyos/` Apache-2.0 完整 App.tsx 树 + `slice/` MIT 宿主桥 + `modules.json` 对照清单)。`--mode slice` 保留原先 org-ui SPA + `FREEOS_UPSTREAM` 反代。工作室智能体对话不导出;组织沟通协作在完整树。说明见 `docs/org-export.md`。
- Organization P1.1(双向资产总线):`freeos org loop run` / `POST /api/org-module/loop/run` / `assets apply` 在托管组织运行时、`OPENXYOS_BASE_URL` 或 `{FREEOS_HOME}/org-os/runtime.json` 可用时,把资产包 ingest 进当前**组织工作区租户**(`remote_applied=true`),不再只写 `openxyos-mirror/`。无运行时地址时仍为镜像-only,且不拉起 Node。回流后同事仍登记为 `org-<slug>` 聊天智能体。FreeOS 工作室账号与组织房间身份分开。说明见 `docs/asset-loop.md`。
Expand Down
10 changes: 10 additions & 0 deletions dashboard/src/api/modules/orgModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface OrgCapability {
description: string;
description_zh: string;
locked: boolean;
delivery?: string;
host_path?: string;
}

export interface OrgModuleStatus {
Expand Down Expand Up @@ -44,6 +46,9 @@ export interface OrgPlaneCounts {
skill_packages: number;
mcp: number;
tasks: number;
directory_employees?: number;
talent_available?: number;
pending_pauses?: number;
}

export interface OrgControlPlane {
Expand Down Expand Up @@ -96,6 +101,11 @@ export interface OrgOverview {
notes: string[];
catalog: OrgCapability[];
module_toggles?: Record<string, boolean>;
governance?: {
pending_pauses?: number;
enabled?: boolean;
href?: string;
};
}

export interface OrgSidecarStart {
Expand Down
12 changes: 10 additions & 2 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"title": "Organization",
"subtitle": "A dual loop: FreeOS does the work, openXYOS governs the org — they grow each other.",
"heroTitle": "Organization",
"heroStory": "FreeOS does the work. openXYOS owns organization and governance. They grow each other.",
"heroStory": "FreeOS does the work. Organization capabilities live in this room of the studio. They grow each other.",
"roomDoorHint": "Organization is another room you can furnish on its own: try, rehearse, and grow your workflows. It shares the studio with everyday chat, and each door keeps its own space.",
"roomFrameTitle": "Organization room",
"toggleLabel": "Enable organization module",
Expand Down Expand Up @@ -176,6 +176,14 @@
"catalogToggleHint": "When off, the module disappears from the openXYOS preview sidebar.",
"autoStartingSidecar": "Opening local openXYOS…",
"glossary": "Employees: already assigned to a department. Colleagues: peers in the same organization (use this word in group chat). Experts / smart helpers: not yet placed in a department — manage them on Experts.",
"roomHint": "This is the organization room — you can arrange it on its own, next to everyday studio chat. The two ways in stay apart.",
"pauseBanner": "{{count}} governance pause(s) waiting for a human decision.",
"pauseBannerAction": "Open governance",
"originalAppHint": "Unmigrated App verticals (workflows, contracts, …) still use the original preview — a transition, not the home.",
"originalAppAction": "Open original App (transition)",
"catalogNative": "Host page",
"catalogOriginalApp": "Original App",
"metricPauses": "Pending pauses",
"receiptTitle": "What just happened",
"assembleReceipt": "Imported {{count}} employee(s) assigned to a department. In group chat call them colleagues, not experts.",
"assembleNext": "Next: open Conversations and invite colleagues to a group. Unassigned helpers stay on Experts.",
Expand All @@ -202,7 +210,7 @@
"orgChartBody": "Departments and people on the host org tree. This page lives in the host — no sidecar or second port.",
"openOrgChart": "Open org chart",
"employeesTitle": "Employees",
"employeesBody": "Directory of people and AI colleagues on the host org tree. Same store as the org chart — no sidecar or second port.",
"employeesBody": "Directory of people and AI colleagues, plus the host talent market. Same store as the org chart — no sidecar.",
"openEmployees": "Open employees",
"skillsTitle": "Skills",
"skillsBody": "Organization skills are generated wrappers for org APIs. Agent Skills stay on FreeOS Personalization — no second skill runtime or sidecar.",
Expand Down
12 changes: 10 additions & 2 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"title": "组织",
"subtitle": "FreeOS 数据面与 openXYOS 控制面的双循环:干活与治理互相生长。",
"heroTitle": "组织",
"heroStory": "FreeOS 负责干活,openXYOS 负责组织与治理,两者互相生长。",
"heroStory": "FreeOS 负责干活。组织能力在这间可单独布置的房间里,两者互相生长。",
"roomDoorHint": "组织像工作室里另一间可独立布置的房间:试用、演练、长出自己的流程;与日常对话同在一个 FreeOS 里,又各自留白。",
"roomFrameTitle": "组织房间",
"toggleLabel": "启用组织模块",
Expand Down Expand Up @@ -176,6 +176,14 @@
"catalogToggleHint": "关闭后,该模块会从右侧 openXYOS 预览的侧栏消失。",
"autoStartingSidecar": "正在打开本机 openXYOS…",
"glossary": "员工:已经编入某个部门。同事:同一组织里一起干活的人(群聊里这样叫)。专家 / 智能助手:还没编入部门的帮手,在「专家」页管理。",
"roomHint": "这里是组织房间:可以单独布置,和日常工作室同属一个 FreeOS,两套入口互不合并。",
"pauseBanner": "有 {{count}} 条治理暂停等待人工决定。",
"pauseBannerAction": "打开治理",
"originalAppHint": "尚未迁入的 App 垂类(流程、合同等)仍走原预览——只是过渡,不是首页。",
"originalAppAction": "打开原 App(过渡)",
"catalogNative": "宿主页",
"catalogOriginalApp": "原 App",
"metricPauses": "待处理暂停",
"receiptTitle": "刚才的结果",
"assembleReceipt": "已导入 {{count}} 名员工(已入编部门)。群聊里请称他们为同事,不要叫专家。",
"assembleNext": "下一步:打开「对话」建群,把同事拉进来。还没入编的帮手在「专家」页。",
Expand All @@ -202,7 +210,7 @@
"orgChartBody": "宿主内的部门与人员树。此页跑在宿主内,不需要边车或第二端口。",
"openOrgChart": "打开组织架构",
"employeesTitle": "员工目录",
"employeesBody": "宿主组织树上的人机员工名册。与组织架构共用同一份数据,不需要边车或第二端口。",
"employeesBody": "已入编的人机员工名册,以及宿主人才市场。与组织架构同一份数据,不需要边车。",
"openEmployees": "打开员工目录",
"skillsTitle": "技能",
"skillsBody": "组织技能是调用组织 API 的生成包装。智能体真正执行的仍是「个性化」里的 Agent Skills,没有第二套运行时或边车。",
Expand Down
14 changes: 14 additions & 0 deletions dashboard/src/org-ui/api/createClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ describe("createOrgApiClient", () => {
],
};
}
if (path === "/org-module/talent") {
return {
success: true,
data: [
{
id: 7,
name: "Policy Analyst",
talent_type: "ai",
status: "available",
},
],
};
}
if (path === "/org-module/governance/pauses") {
return {
pauses: [{ pause_id: "p1", status: "pending", tool_name: "delete" }],
Expand Down Expand Up @@ -133,6 +146,7 @@ describe("createOrgApiClient", () => {
expect(await client.announcements.unread()).toEqual({ count: 2 });
expect((await client.org.tree())[0]?.name).toBe("HQ");
expect((await client.employees.list())[0]?.name).toBe("Ada");
expect((await client.talent.list())[0]?.name).toBe("Policy Analyst");
expect((await client.governance.pauses()).pauses[0]?.pause_id).toBe("p1");
expect((await client.skills.list()).skills[0]?.slug).toBe("org-employees");
expect((await client.knowledge.list()).bases[0]?.name).toBe("Policies");
Expand Down
96 changes: 96 additions & 0 deletions dashboard/src/org-ui/api/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,51 @@ export interface OrgEmployeesClient {
remove(id: number): Promise<void>;
}

export interface OrgTalent {
id: number;
name: string;
talent_type: string;
skills?: string;
description?: string;
source?: string;
status: string;
agent_type?: string | null;
slug?: string | null;
avatar_emoji?: string;
category?: string;
employee_id?: number | null;
}

export interface TalentStats {
total: number;
ai: number;
human: number;
byCategory: { category: string; count: number }[];
}

export interface TalentListParams {
type?: string;
status?: string;
search?: string;
}

export interface OrgTalentRecruitResult {
talent: OrgTalent;
employee: OrgEmployee;
created: boolean;
colleague_slug?: string;
}

export interface OrgTalentClient {
list(params?: TalentListParams): Promise<OrgTalent[]>;
get(id: number): Promise<OrgTalent>;
stats(): Promise<TalentStats>;
recruit(
id: number,
departmentId?: number | null,
): Promise<OrgTalentRecruitResult>;
}

export type GovernancePauseStatus =
| "pending"
| "approved"
Expand Down Expand Up @@ -508,6 +553,8 @@ export interface OrgCapability {
description: string;
description_zh: string;
locked: boolean;
delivery?: string;
host_path?: string;
}

export interface OrgPrefs {
Expand Down Expand Up @@ -642,6 +689,9 @@ export interface OrgWorkspaceFreeos {
skill_packages?: number;
mcp?: number;
tasks?: number;
directory_employees?: number;
talent_available?: number;
pending_pauses?: number;
}

export interface OrgWorkspaceOpenxyos {
Expand Down Expand Up @@ -674,6 +724,11 @@ export interface OrgWorkspaceOverview {
catalog?: OrgCapability[];
module_toggles?: Record<string, boolean>;
notes?: string[];
governance?: {
pending_pauses?: number;
enabled?: boolean;
href?: string;
};
}

export interface OrgWorkspaceClient {
Expand All @@ -684,6 +739,7 @@ export interface OrgApiClient {
announcements: OrgAnnouncementsClient;
org: OrgChartClient;
employees: OrgEmployeesClient;
talent: OrgTalentClient;
governance: OrgGovernanceClient;
skills: OrgSkillsClient;
knowledge: OrgKnowledgeClient;
Expand Down Expand Up @@ -717,6 +773,7 @@ export function createOrgApiClient(opts: {
settingsPrefix?: string;
agentsPrefix?: string;
workspacePrefix?: string;
talentPrefix?: string;
}): OrgApiClient {
const prefix = opts.announcementsPrefix ?? "/org-module/announcements";
const orgPrefix = opts.orgPrefix ?? "/org-module/org";
Expand All @@ -728,6 +785,7 @@ export function createOrgApiClient(opts: {
const settingsPrefix = opts.settingsPrefix ?? "/org-module";
const agentsPrefix = opts.agentsPrefix ?? "/org-module";
const workspacePrefix = opts.workspacePrefix ?? "/org-module";
const talentPrefix = opts.talentPrefix ?? "/org-module/talent";
const fetchJson = opts.fetchJson;

const announcements: OrgAnnouncementsClient = {
Expand Down Expand Up @@ -930,6 +988,43 @@ export function createOrgApiClient(opts: {
},
};

const talent: OrgTalentClient = {
async list(params = {}) {
const query = new URLSearchParams();
if (params.type) query.set("type", params.type);
if (params.status) query.set("status", params.status);
if (params.search) query.set("search", params.search);
const suffix = query.toString() ? `?${query.toString()}` : "";
return unwrap(
await fetchJson<OrgEnvelope<OrgTalent[]>>(`${talentPrefix}${suffix}`),
);
},
async get(id) {
return unwrap(
await fetchJson<OrgEnvelope<OrgTalent>>(`${talentPrefix}/${id}`),
);
},
async stats() {
return unwrap(
await fetchJson<OrgEnvelope<TalentStats>>(`${talentPrefix}/stats`),
);
},
async recruit(id, departmentId) {
return unwrap(
await fetchJson<OrgEnvelope<OrgTalentRecruitResult>>(
`${talentPrefix}/${id}/recruit`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(
departmentId == null ? {} : { department_id: departmentId },
),
},
),
);
},
};

const governance: OrgGovernanceClient = {
async pauses(status) {
const query = status ? `?status=${encodeURIComponent(status)}` : "";
Expand Down Expand Up @@ -1279,6 +1374,7 @@ export function createOrgApiClient(opts: {
announcements,
org,
employees,
talent,
governance,
skills,
knowledge,
Expand Down
5 changes: 5 additions & 0 deletions dashboard/src/org-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export type {
OrgEmployee,
HostSkillPackage,
OrgEmployeesClient,
OrgTalent,
OrgTalentClient,
OrgTalentRecruitResult,
TalentListParams,
TalentStats,
OrgEnvelope,
OrgGovernanceClient,
OrgKnowledgeBase,
Expand Down
54 changes: 54 additions & 0 deletions dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,58 @@ describe("EmployeesPage", () => {
);
});
});

it("recruits host talent without an iframe", async () => {
const client = mockClient();
const talent = {
list: vi.fn(async () => [
{
id: 3,
name: "Policy Analyst",
talent_type: "ai",
status: "available",
skills: "policy",
},
]),
get: vi.fn(),
stats: vi.fn(async () => ({
total: 1,
ai: 1,
human: 0,
byCategory: [],
})),
recruit: vi.fn(async () => ({
talent: { id: 3, name: "Policy Analyst", status: "recruited" },
employee: {
id: 11,
name: "Policy Analyst",
role: "",
employee_type: "ai",
department_id: 1,
status: "active",
},
created: true,
colleague_slug: "policy-analyst",
})),
};
render(
<EmployeesPage
client={client}
talent={talent}
session={{
userId: 1,
displayName: "Ada",
role: "admin",
isAdmin: true,
}}
locale="en"
/>,
);
fireEvent.click(await screen.findByText("Talent market"));
expect(await screen.findByTestId("org-talent-card-3")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("org-talent-recruit-3"));
await waitFor(() => {
expect(talent.recruit).toHaveBeenCalledWith(3, 1);
});
});
});
Loading
Loading