@octo-cyber/kinship-calc 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +22 -0
  2. package/dist/controllers/family-tree.controller.d.ts +27 -0
  3. package/dist/controllers/family-tree.controller.d.ts.map +1 -0
  4. package/dist/controllers/family-tree.controller.js +88 -0
  5. package/dist/controllers/family-tree.controller.js.map +1 -0
  6. package/dist/controllers/kinship-calc.controller.d.ts +15 -0
  7. package/dist/controllers/kinship-calc.controller.d.ts.map +1 -0
  8. package/dist/controllers/kinship-calc.controller.js +45 -0
  9. package/dist/controllers/kinship-calc.controller.js.map +1 -0
  10. package/dist/engine/index.d.ts +7 -0
  11. package/dist/engine/index.d.ts.map +1 -0
  12. package/dist/engine/index.js +15 -0
  13. package/dist/engine/index.js.map +1 -0
  14. package/dist/engine/kinship-data.d.ts +31 -0
  15. package/dist/engine/kinship-data.d.ts.map +1 -0
  16. package/dist/engine/kinship-data.js +150 -0
  17. package/dist/engine/kinship-data.js.map +1 -0
  18. package/dist/engine/kinship-engine.d.ts +44 -0
  19. package/dist/engine/kinship-engine.d.ts.map +1 -0
  20. package/dist/engine/kinship-engine.js +184 -0
  21. package/dist/engine/kinship-engine.js.map +1 -0
  22. package/dist/engine/relation-path.d.ts +37 -0
  23. package/dist/engine/relation-path.d.ts.map +1 -0
  24. package/dist/engine/relation-path.js +60 -0
  25. package/dist/engine/relation-path.js.map +1 -0
  26. package/dist/entities/family-tree-person.entity.d.ts +12 -0
  27. package/dist/entities/family-tree-person.entity.d.ts.map +1 -0
  28. package/dist/entities/family-tree-person.entity.js +61 -0
  29. package/dist/entities/family-tree-person.entity.js.map +1 -0
  30. package/dist/entities/family-tree-relation.entity.d.ts +15 -0
  31. package/dist/entities/family-tree-relation.entity.d.ts.map +1 -0
  32. package/dist/entities/family-tree-relation.entity.js +58 -0
  33. package/dist/entities/family-tree-relation.entity.js.map +1 -0
  34. package/dist/entities/family-tree.entity.d.ts +9 -0
  35. package/dist/entities/family-tree.entity.d.ts.map +1 -0
  36. package/dist/entities/family-tree.entity.js +50 -0
  37. package/dist/entities/family-tree.entity.js.map +1 -0
  38. package/dist/entities/index.d.ts +12 -0
  39. package/dist/entities/index.d.ts.map +1 -0
  40. package/dist/entities/index.js +22 -0
  41. package/dist/entities/index.js.map +1 -0
  42. package/dist/entities/kinship-title.entity.d.ts +18 -0
  43. package/dist/entities/kinship-title.entity.d.ts.map +1 -0
  44. package/dist/entities/kinship-title.entity.js +59 -0
  45. package/dist/entities/kinship-title.entity.js.map +1 -0
  46. package/dist/index.d.ts +12 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +33 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/kinship-calc.module.d.ts +9 -0
  51. package/dist/kinship-calc.module.d.ts.map +1 -0
  52. package/dist/kinship-calc.module.js +45 -0
  53. package/dist/kinship-calc.module.js.map +1 -0
  54. package/dist/schemas/kinship.schema.d.ts +129 -0
  55. package/dist/schemas/kinship.schema.d.ts.map +1 -0
  56. package/dist/schemas/kinship.schema.js +50 -0
  57. package/dist/schemas/kinship.schema.js.map +1 -0
  58. package/dist/services/family-tree.service.d.ts +38 -0
  59. package/dist/services/family-tree.service.d.ts.map +1 -0
  60. package/dist/services/family-tree.service.js +179 -0
  61. package/dist/services/family-tree.service.js.map +1 -0
  62. package/dist/services/kinship-calc.service.d.ts +26 -0
  63. package/dist/services/kinship-calc.service.d.ts.map +1 -0
  64. package/dist/services/kinship-calc.service.js +66 -0
  65. package/dist/services/kinship-calc.service.js.map +1 -0
  66. package/package.json +60 -0
  67. package/src/controllers/family-tree.controller.ts +102 -0
  68. package/src/controllers/kinship-calc.controller.ts +50 -0
  69. package/src/engine/index.ts +6 -0
  70. package/src/engine/kinship-data.ts +188 -0
  71. package/src/engine/kinship-engine.ts +230 -0
  72. package/src/engine/relation-path.ts +63 -0
  73. package/src/entities/family-tree-person.entity.ts +37 -0
  74. package/src/entities/family-tree-relation.entity.ts +36 -0
  75. package/src/entities/family-tree.entity.ts +28 -0
  76. package/src/entities/index.ts +18 -0
  77. package/src/entities/kinship-title.entity.ts +38 -0
  78. package/src/index.ts +33 -0
  79. package/src/kinship-calc.module.ts +51 -0
  80. package/src/schemas/kinship.schema.ts +70 -0
  81. package/src/services/family-tree.service.ts +211 -0
  82. package/src/services/kinship-calc.service.ts +68 -0
  83. package/tsconfig.build.json +4 -0
  84. package/tsconfig.json +10 -0
  85. package/web/components/FamilyTreeCanvas.tsx +177 -0
  86. package/web/components/FamilyTreeDialogs.tsx +275 -0
  87. package/web/components/KinshipResultCard.tsx +98 -0
  88. package/web/components/RegionSelector.tsx +32 -0
  89. package/web/components/RelationChainBuilder.tsx +104 -0
  90. package/web/index.ts +29 -0
  91. package/web/manifest.ts +24 -0
  92. package/web/messages/en-US.json +108 -0
  93. package/web/messages/zh-CN.json +108 -0
  94. package/web/pages/FamilyTreePage.tsx +240 -0
  95. package/web/pages/KinshipCalcPage.tsx +140 -0
  96. package/web/services/kinship-service.ts +140 -0
  97. package/web/stores/kinship-store.ts +80 -0
  98. package/web/types/kinship.ts +85 -0
@@ -0,0 +1,26 @@
1
+ import { type KinshipResult, type Region } from '../engine/index.js';
2
+ import type { CalculateKinshipDto, PathFromChainDto } from '../schemas/kinship.schema.js';
3
+ export declare class KinshipCalcService {
4
+ private readonly logger;
5
+ initialize(): void;
6
+ calculate(dto: CalculateKinshipDto): KinshipResult;
7
+ calculateFromChain(dto: PathFromChainDto): KinshipResult;
8
+ /** Check if two paths to the same person create a generational conflict */
9
+ checkConflict(paths: string[]): {
10
+ conflict: boolean;
11
+ message: string | null;
12
+ };
13
+ /** Return all known relation paths with their standard titles */
14
+ listAllTitles(region?: Region): Array<{
15
+ path: string;
16
+ title: string;
17
+ generationDelta: number;
18
+ }>;
19
+ /** Return available relation atoms for building chains */
20
+ getRelationAtoms(): Array<{
21
+ code: string;
22
+ label: string;
23
+ generationDelta: number;
24
+ }>;
25
+ }
26
+ //# sourceMappingURL=kinship-calc.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kinship-calc.service.d.ts","sourceRoot":"","sources":["../../src/services/kinship-calc.service.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAEzF,qBACa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IAEtD,UAAU,IAAI,IAAI;IAIlB,SAAS,CAAC,GAAG,EAAE,mBAAmB,GAAG,aAAa;IAOlD,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa;IAQxD,2EAA2E;IAC3E,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAK7E,iEAAiE;IACjE,aAAa,CAAC,MAAM,GAAE,MAAmB,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAW3G,0DAA0D;IAC1D,gBAAgB,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;CAcpF"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.KinshipCalcService = void 0;
10
+ const core_1 = require("@octo-cyber/core");
11
+ const index_js_1 = require("../engine/index.js");
12
+ let KinshipCalcService = class KinshipCalcService {
13
+ logger = core_1.Container.get(core_1.LoggerService);
14
+ initialize() {
15
+ this.logger.info('KinshipCalcService initialized');
16
+ }
17
+ calculate(dto) {
18
+ return (0, index_js_1.calculateKinship)(dto.path, {
19
+ region: dto.region,
20
+ egoGender: dto.egoGender,
21
+ });
22
+ }
23
+ calculateFromChain(dto) {
24
+ const path = (0, index_js_1.encodePath)(dto.chain);
25
+ return (0, index_js_1.calculateKinship)(path, {
26
+ region: dto.region,
27
+ egoGender: dto.egoGender,
28
+ });
29
+ }
30
+ /** Check if two paths to the same person create a generational conflict */
31
+ checkConflict(paths) {
32
+ const message = (0, index_js_1.detectGenerationConflict)(paths);
33
+ return { conflict: message !== null, message };
34
+ }
35
+ /** Return all known relation paths with their standard titles */
36
+ listAllTitles(region = 'standard') {
37
+ return index_js_1.KINSHIP_TABLE.map((entry) => {
38
+ const result = (0, index_js_1.calculateKinship)(entry.path, { region });
39
+ return {
40
+ path: entry.path,
41
+ title: result.title,
42
+ generationDelta: result.generationDelta,
43
+ };
44
+ });
45
+ }
46
+ /** Return available relation atoms for building chains */
47
+ getRelationAtoms() {
48
+ return [
49
+ { code: 'F', label: '父亲', generationDelta: 1 },
50
+ { code: 'M', label: '母亲', generationDelta: 1 },
51
+ { code: 'S', label: '儿子', generationDelta: -1 },
52
+ { code: 'D', label: '女儿', generationDelta: -1 },
53
+ { code: 'B+', label: '哥哥', generationDelta: 0 },
54
+ { code: 'B-', label: '弟弟', generationDelta: 0 },
55
+ { code: 'Z+', label: '姐姐', generationDelta: 0 },
56
+ { code: 'Z-', label: '妹妹', generationDelta: 0 },
57
+ { code: 'H', label: '丈夫', generationDelta: 0 },
58
+ { code: 'W', label: '妻子', generationDelta: 0 },
59
+ ];
60
+ }
61
+ };
62
+ exports.KinshipCalcService = KinshipCalcService;
63
+ exports.KinshipCalcService = KinshipCalcService = __decorate([
64
+ (0, core_1.Service)()
65
+ ], KinshipCalcService);
66
+ //# sourceMappingURL=kinship-calc.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kinship-calc.service.js","sourceRoot":"","sources":["../../src/services/kinship-calc.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAoE;AACpE,iDAO2B;AAIpB,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IACZ,MAAM,GAAG,gBAAS,CAAC,GAAG,CAAC,oBAAa,CAAC,CAAA;IAEtD,UAAU;QACR,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;IACpD,CAAC;IAED,SAAS,CAAC,GAAwB;QAChC,OAAO,IAAA,2BAAgB,EAAC,GAAG,CAAC,IAAI,EAAE;YAChC,MAAM,EAAE,GAAG,CAAC,MAAgB;YAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;SACzB,CAAC,CAAA;IACJ,CAAC;IAED,kBAAkB,CAAC,GAAqB;QACtC,MAAM,IAAI,GAAG,IAAA,qBAAU,EAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAClC,OAAO,IAAA,2BAAgB,EAAC,IAAI,EAAE;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAgB;YAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;SACzB,CAAC,CAAA;IACJ,CAAC;IAED,2EAA2E;IAC3E,aAAa,CAAC,KAAe;QAC3B,MAAM,OAAO,GAAG,IAAA,mCAAwB,EAAC,KAAK,CAAC,CAAA;QAC/C,OAAO,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,EAAE,OAAO,EAAE,CAAA;IAChD,CAAC;IAED,iEAAiE;IACjE,aAAa,CAAC,SAAiB,UAAU;QACvC,OAAO,wBAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACjC,MAAM,MAAM,GAAG,IAAA,2BAAgB,EAAC,KAAK,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;YACvD,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,eAAe,EAAE,MAAM,CAAC,eAAe;aACxC,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,0DAA0D;IAC1D,gBAAgB;QACd,OAAO;YACL,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,CAAC,EAAE;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,CAAC,EAAE;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;SAClD,CAAA;IACH,CAAC;CACF,CAAA;AAvDY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,cAAO,GAAE;GACG,kBAAkB,CAuD9B"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@octo-cyber/kinship-calc",
3
+ "version": "0.5.2",
4
+ "description": "辈分计算器 — 输入关系链,自动计算亲属称呼,支持南北方差异与方言",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./web": {
14
+ "types": "./web/index.ts",
15
+ "import": "./web/index.ts",
16
+ "default": "./web/index.ts"
17
+ },
18
+ "./web/pages/*": {
19
+ "types": "./web/pages/*.tsx",
20
+ "import": "./web/pages/*.tsx",
21
+ "default": "./web/pages/*.tsx"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "@octo-cyber/core": "^0.5.4",
26
+ "@octo-cyber/auth": "^0.5.6",
27
+ "@octo-cyber/ui": "^0.5.3"
28
+ },
29
+ "peerDependencies": {
30
+ "next": ">=15",
31
+ "next-intl": ">=3",
32
+ "react": ">=19",
33
+ "react-dom": ">=19",
34
+ "zod": ">=3"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "@types/react": "^19",
39
+ "typescript": "^5.8.0"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/jefflower/octo-kinship-calc.git"
44
+ },
45
+ "publishMeta": {
46
+ "repository": "jefflower/octo-kinship-calc",
47
+ "branch": "main",
48
+ "commitHash": "7490bd3995fad90547d8d8d7febf4143142e5b35",
49
+ "publishedAt": "2026-03-27T15:19:48.392Z",
50
+ "publishedFrom": "monorepo:packages/kinship-calc"
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc -p tsconfig.build.json",
57
+ "dev": "tsc -p tsconfig.build.json --watch",
58
+ "typecheck": "tsc --noEmit"
59
+ }
60
+ }
@@ -0,0 +1,102 @@
1
+ import type { Request, Response, RequestHandler } from '@octo-cyber/core'
2
+ import { Container, ApiResponse, asyncHandler, AppError } from '@octo-cyber/core'
3
+ import { FamilyTreeService } from '../services/family-tree.service.js'
4
+ import {
5
+ CreateFamilyTreeSchema,
6
+ UpdateFamilyTreeSchema,
7
+ CreatePersonSchema,
8
+ UpdatePersonSchema,
9
+ CreateRelationSchema,
10
+ ResolveTreePathSchema,
11
+ } from '../schemas/kinship.schema.js'
12
+
13
+ type AuthRequest = Request & { user?: { userId: number } }
14
+
15
+ function ownerId(req: Request): number {
16
+ const id = (req as AuthRequest).user?.userId
17
+ if (!id) throw new AppError('未认证', 401)
18
+ return id
19
+ }
20
+
21
+ export class FamilyTreeController {
22
+ private readonly service = Container.get(FamilyTreeService)
23
+
24
+ /** GET /api/v1/kinship/trees */
25
+ listTrees: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
26
+ const trees = await this.service.listTrees(ownerId(req))
27
+ res.json(ApiResponse.ok(trees))
28
+ })
29
+
30
+ /** GET /api/v1/kinship/trees/:id */
31
+ getTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
32
+ const graph = await this.service.getTree(Number(req.params.id), ownerId(req))
33
+ res.json(ApiResponse.ok(graph))
34
+ })
35
+
36
+ /** POST /api/v1/kinship/trees */
37
+ createTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
38
+ const dto = CreateFamilyTreeSchema.parse(req.body)
39
+ const tree = await this.service.createTree(dto, ownerId(req))
40
+ res.status(201).json(ApiResponse.ok(tree))
41
+ })
42
+
43
+ /** PUT /api/v1/kinship/trees/:id */
44
+ updateTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
45
+ const dto = UpdateFamilyTreeSchema.parse(req.body)
46
+ const tree = await this.service.updateTree(Number(req.params.id), dto, ownerId(req))
47
+ res.json(ApiResponse.ok(tree))
48
+ })
49
+
50
+ /** DELETE /api/v1/kinship/trees/:id */
51
+ deleteTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
52
+ await this.service.deleteTree(Number(req.params.id), ownerId(req))
53
+ res.json(ApiResponse.ok(null))
54
+ })
55
+
56
+ /** POST /api/v1/kinship/persons */
57
+ addPerson: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
58
+ const dto = CreatePersonSchema.parse(req.body)
59
+ const person = await this.service.addPerson(dto, ownerId(req))
60
+ res.status(201).json(ApiResponse.ok(person))
61
+ })
62
+
63
+ /** PUT /api/v1/kinship/persons/:id?treeId= */
64
+ updatePerson: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
65
+ const personId = Number(req.params.id)
66
+ const treeId = Number(req.query.treeId)
67
+ if (!treeId) throw new AppError('缺少 treeId 参数', 400)
68
+ const dto = UpdatePersonSchema.parse(req.body)
69
+ const person = await this.service.updatePerson(personId, treeId, dto, ownerId(req))
70
+ res.json(ApiResponse.ok(person))
71
+ })
72
+
73
+ /** DELETE /api/v1/kinship/persons/:id?treeId= */
74
+ deletePerson: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
75
+ const treeId = Number(req.query.treeId)
76
+ if (!treeId) throw new AppError('缺少 treeId 参数', 400)
77
+ await this.service.deletePerson(Number(req.params.id), treeId, ownerId(req))
78
+ res.json(ApiResponse.ok(null))
79
+ })
80
+
81
+ /** POST /api/v1/kinship/relations */
82
+ addRelation: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
83
+ const dto = CreateRelationSchema.parse(req.body)
84
+ const relation = await this.service.addRelation(dto, ownerId(req))
85
+ res.status(201).json(ApiResponse.ok(relation))
86
+ })
87
+
88
+ /** DELETE /api/v1/kinship/relations/:id?treeId= */
89
+ deleteRelation: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
90
+ const treeId = Number(req.query.treeId)
91
+ if (!treeId) throw new AppError('缺少 treeId 参数', 400)
92
+ await this.service.deleteRelation(Number(req.params.id), treeId, ownerId(req))
93
+ res.json(ApiResponse.ok(null))
94
+ })
95
+
96
+ /** POST /api/v1/kinship/resolve */
97
+ resolveKinship: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
98
+ const dto = ResolveTreePathSchema.parse(req.body)
99
+ const result = await this.service.resolveKinship(dto, ownerId(req))
100
+ res.json(ApiResponse.ok(result))
101
+ })
102
+ }
@@ -0,0 +1,50 @@
1
+ import type { Request, Response, RequestHandler } from '@octo-cyber/core'
2
+ import { Container, ApiResponse, asyncHandler, AppError } from '@octo-cyber/core'
3
+ import { KinshipCalcService } from '../services/kinship-calc.service.js'
4
+ import {
5
+ CalculateKinshipSchema,
6
+ PathFromChainSchema,
7
+ } from '../schemas/kinship.schema.js'
8
+ import type { Region } from '../engine/index.js'
9
+
10
+ const VALID_REGIONS: Region[] = ['standard', 'northern', 'southern', 'cantonese', 'minnan', 'wu']
11
+
12
+ export class KinshipCalcController {
13
+ private readonly service = Container.get(KinshipCalcService)
14
+
15
+ /** POST /api/kinship/calculate */
16
+ calculate: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
17
+ const dto = CalculateKinshipSchema.parse(req.body)
18
+ const result = this.service.calculate(dto)
19
+ res.json(ApiResponse.ok(result))
20
+ })
21
+
22
+ /** POST /api/kinship/chain */
23
+ calculateFromChain: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
24
+ const dto = PathFromChainSchema.parse(req.body)
25
+ const result = this.service.calculateFromChain(dto)
26
+ res.json(ApiResponse.ok(result))
27
+ })
28
+
29
+ /** POST /api/kinship/conflict-check */
30
+ checkConflict: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
31
+ const { paths } = req.body as { paths: string[] }
32
+ if (!Array.isArray(paths)) throw AppError.badRequest('paths 必须是数组')
33
+ const result = this.service.checkConflict(paths)
34
+ res.json(ApiResponse.ok(result))
35
+ })
36
+
37
+ /** GET /api/kinship/titles?region=standard */
38
+ listTitles: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
39
+ const region = ((req.query.region as string) || 'standard') as Region
40
+ if (!VALID_REGIONS.includes(region)) throw AppError.badRequest(`不支持的地区:${region}`)
41
+ const list = this.service.listAllTitles(region)
42
+ res.json(ApiResponse.ok(list))
43
+ })
44
+
45
+ /** GET /api/kinship/atoms */
46
+ getAtoms: RequestHandler = asyncHandler(async (_req: Request, res: Response) => {
47
+ const atoms = this.service.getRelationAtoms()
48
+ res.json(ApiResponse.ok(atoms))
49
+ })
50
+ }
@@ -0,0 +1,6 @@
1
+ export { calculateKinship, detectGenerationConflict } from './kinship-engine.js'
2
+ export type { KinshipResult, CalcOptions, Region } from './kinship-engine.js'
3
+ export { KINSHIP_TABLE, KINSHIP_LOOKUP } from './kinship-data.js'
4
+ export type { KinshipEntry } from './kinship-data.js'
5
+ export { encodePath, decodePath, pathGenerationDelta, atomGender } from './relation-path.js'
6
+ export type { RelationAtom, EgoGender } from './relation-path.js'
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Comprehensive Chinese kinship term lookup table.
3
+ *
4
+ * Each entry: { path, standard, northern?, southern?, cantonese?, minnan?, wu?, reverse?, note? }
5
+ *
6
+ * path: encoded relation path (dot-separated atoms)
7
+ * standard: standard Mandarin title (普通话)
8
+ * northern: northern dialect variant (北方)
9
+ * southern: southern Mandarin variant (南方官话)
10
+ * cantonese: Cantonese variant (粤语)
11
+ * minnan: Min-nan / Hokkien variant (闽南语)
12
+ * wu: Wu dialect variant (吴语/上海话)
13
+ * reverse: how the relative addresses ego (男/女 separated by /)
14
+ * note: explanatory note
15
+ */
16
+
17
+ export interface KinshipEntry {
18
+ path: string
19
+ standard: string
20
+ northern?: string
21
+ southern?: string
22
+ cantonese?: string
23
+ minnan?: string
24
+ wu?: string
25
+ reverse?: string // e.g. "孙子/孙女" (male ego / female ego)
26
+ note?: string
27
+ }
28
+
29
+ export const KINSHIP_TABLE: KinshipEntry[] = [
30
+ // ── 直系 ─────────────────────────────────────────────────────────
31
+
32
+ // Parents
33
+ { path: 'F', standard: '父亲', northern: '爸', cantonese: '爸爸', reverse: '孩子', note: '父' },
34
+ { path: 'M', standard: '母亲', northern: '妈', cantonese: '妈妈', reverse: '孩子', note: '母' },
35
+
36
+ // Grandparents (paternal)
37
+ { path: 'F.F', standard: '祖父', northern: '爷爷', southern: '公公', cantonese: '爺爺', minnan: '阿公', wu: '阿爷', reverse: '孙子/孙女', note: '父父' },
38
+ { path: 'F.M', standard: '祖母', northern: '奶奶', southern: '婆婆', cantonese: '嫲嫲', minnan: '阿嬷', wu: '阿嬶', reverse: '孙子/孙女', note: '父母' },
39
+
40
+ // Grandparents (maternal)
41
+ { path: 'M.F', standard: '外祖父', northern: '姥爷', southern: '外公', cantonese: '外公', minnan: '阿公', wu: '外公', reverse: '外孙/外孙女', note: '母父' },
42
+ { path: 'M.M', standard: '外祖母', northern: '姥姥', southern: '外婆', cantonese: '外婆', minnan: '阿妈', wu: '外婆', reverse: '外孙/外孙女', note: '母母' },
43
+
44
+ // Great-grandparents (paternal)
45
+ { path: 'F.F.F', standard: '曾祖父', northern: '太爷爷', reverse: '曾孙/曾孙女' },
46
+ { path: 'F.F.M', standard: '曾祖母', northern: '太奶奶', reverse: '曾孙/曾孙女' },
47
+ { path: 'M.M.F', standard: '外曾祖父', northern: '太姥爷', reverse: '曾外孙/曾外孙女' },
48
+ { path: 'M.M.M', standard: '外曾祖母', northern: '太姥姥', reverse: '曾外孙/曾外孙女' },
49
+
50
+ // Great-great-grandparents
51
+ { path: 'F.F.F.F', standard: '高祖父', reverse: '玄孙/玄孙女' },
52
+ { path: 'F.F.F.M', standard: '高祖母', reverse: '玄孙/玄孙女' },
53
+
54
+ // Children
55
+ { path: 'S', standard: '儿子', northern: '儿子', cantonese: '仔', minnan: '囝', reverse: '父亲/母亲' },
56
+ { path: 'D', standard: '女儿', northern: '闺女', cantonese: '囡', reverse: '父亲/母亲' },
57
+
58
+ // Grandchildren (paternal line)
59
+ { path: 'S.S', standard: '孙子', cantonese: '孙仔', reverse: '祖父/祖母' },
60
+ { path: 'S.D', standard: '孙女', cantonese: '孙囡', reverse: '祖父/祖母' },
61
+ { path: 'D.S', standard: '外孙', reverse: '外祖父/外祖母' },
62
+ { path: 'D.D', standard: '外孙女', reverse: '外祖父/外祖母' },
63
+
64
+ // Great-grandchildren
65
+ { path: 'S.S.S', standard: '曾孙', reverse: '曾祖父/曾祖母' },
66
+ { path: 'S.S.D', standard: '曾孙女', reverse: '曾祖父/曾祖母' },
67
+
68
+ // Spouse
69
+ { path: 'H', standard: '丈夫', northern: '老公', cantonese: '老公', reverse: '妻子/老婆' },
70
+ { path: 'W', standard: '妻子', northern: '老婆', cantonese: '老婆', reverse: '丈夫/老公' },
71
+
72
+ // ── 旁系 (collateral) ────────────────────────────────────────────
73
+
74
+ // Father's brothers
75
+ { path: 'F.B+', standard: '伯父', northern: '大爷', cantonese: '伯父', minnan: '阿伯', wu: '伯伯', reverse: '侄子/侄女', note: '父之兄' },
76
+ { path: 'F.B-', standard: '叔父', northern: '叔叔', cantonese: '叔父', minnan: '阿叔', wu: '叔叔', reverse: '侄子/侄女', note: '父之弟' },
77
+
78
+ // Father's sisters
79
+ { path: 'F.Z+', standard: '姑母', northern: '大姑', cantonese: '姑母', minnan: '阿姑', wu: '姑妈', reverse: '侄子/侄女', note: '父之姐' },
80
+ { path: 'F.Z-', standard: '姑母', northern: '姑姑', cantonese: '姑母', minnan: '阿姑', wu: '姑姑', reverse: '侄子/侄女', note: '父之妹' },
81
+
82
+ // Mother's brothers
83
+ { path: 'M.B+', standard: '舅父', northern: '大舅', cantonese: '舅父', minnan: '阿舅', wu: '舅舅', reverse: '外甥/外甥女', note: '母之兄' },
84
+ { path: 'M.B-', standard: '舅父', northern: '舅舅', cantonese: '舅父', minnan: '阿舅', wu: '舅舅', reverse: '外甥/外甥女', note: '母之弟' },
85
+
86
+ // Mother's sisters
87
+ { path: 'M.Z+', standard: '姨母', northern: '大姨', cantonese: '姨母', minnan: '阿姨', wu: '阿姨', reverse: '外甥/外甥女', note: '母之姐' },
88
+ { path: 'M.Z-', standard: '姨母', northern: '阿姨', cantonese: '姨母', minnan: '阿姨', wu: '阿姨', reverse: '外甥/外甥女', note: '母之妹' },
89
+
90
+ // Siblings
91
+ { path: 'B+', standard: '兄长', northern: '哥哥', cantonese: '大佬', minnan: '阿兄', wu: '阿哥', reverse: '弟弟/妹妹' },
92
+ { path: 'B-', standard: '弟弟', northern: '弟弟', cantonese: '细佬', minnan: '小弟', wu: '阿弟', reverse: '哥哥/姐姐' },
93
+ { path: 'Z+', standard: '姐姐', northern: '姐姐', cantonese: '家姐', minnan: '阿姐', wu: '阿姐', reverse: '弟弟/妹妹' },
94
+ { path: 'Z-', standard: '妹妹', northern: '妹妹', cantonese: '妹妹', minnan: '小妹', wu: '阿妹', reverse: '哥哥/姐姐' },
95
+
96
+ // Paternal cousins (堂)
97
+ { path: 'F.B+.S', standard: '堂兄', northern: '堂哥', note: '伯父之子(年长)' },
98
+ { path: 'F.B+.D', standard: '堂姐', northern: '堂姐', note: '伯父之女(年长)' },
99
+ { path: 'F.B-.S', standard: '堂弟', northern: '堂弟', note: '叔叔之子(年幼)' },
100
+ { path: 'F.B-.D', standard: '堂妹', northern: '堂妹', note: '叔叔之女(年幼)' },
101
+ { path: 'F.Z+.S', standard: '表兄', northern: '表哥', note: '姑母之子(年长)' },
102
+ { path: 'F.Z+.D', standard: '表姐', northern: '表姐', note: '姑母之女(年长)' },
103
+ { path: 'F.Z-.S', standard: '表弟', northern: '表弟', note: '姑母之子(年幼)' },
104
+ { path: 'F.Z-.D', standard: '表妹', northern: '表妹', note: '姑母之女(年幼)' },
105
+
106
+ // Maternal cousins (表)
107
+ { path: 'M.B+.S', standard: '表兄', northern: '表哥', note: '舅父之子(年长)' },
108
+ { path: 'M.B+.D', standard: '表姐', northern: '表姐', note: '舅父之女(年长)' },
109
+ { path: 'M.B-.S', standard: '表弟', northern: '表弟', note: '舅父之子(年幼)' },
110
+ { path: 'M.B-.D', standard: '表妹', northern: '表妹', note: '舅父之女(年幼)' },
111
+ { path: 'M.Z+.S', standard: '表兄', northern: '表哥', note: '姨母之子(年长)' },
112
+ { path: 'M.Z+.D', standard: '表姐', northern: '表姐', note: '姨母之女(年长)' },
113
+ { path: 'M.Z-.S', standard: '表弟', northern: '表弟', note: '姨母之子(年幼)' },
114
+ { path: 'M.Z-.D', standard: '表妹', northern: '表妹', note: '姨母之女(年幼)' },
115
+
116
+ // Nephews/nieces (siblings' children)
117
+ { path: 'B+.S', standard: '侄子', reverse: '伯父/伯母' },
118
+ { path: 'B+.D', standard: '侄女', reverse: '伯父/伯母' },
119
+ { path: 'B-.S', standard: '侄子', reverse: '叔叔/婶婶' },
120
+ { path: 'B-.D', standard: '侄女', reverse: '叔叔/婶婶' },
121
+ { path: 'Z+.S', standard: '外甥', northern: '外甥', note: '姐之子', reverse: '舅父/舅母' },
122
+ { path: 'Z+.D', standard: '外甥女', northern: '外甥女', note: '姐之女', reverse: '舅父/舅母' },
123
+ { path: 'Z-.S', standard: '外甥', northern: '外甥', note: '妹之子', reverse: '舅父/舅母' },
124
+ { path: 'Z-.D', standard: '外甥女', northern: '外甥女', note: '妹之女', reverse: '舅父/舅母' },
125
+
126
+ // ── 姻亲 (in-laws) ───────────────────────────────────────────────
127
+
128
+ // Spouse's parents
129
+ { path: 'W.F', standard: '岳父', northern: '老丈人', cantonese: '外父', wu: '丈人', reverse: '女婿' },
130
+ { path: 'W.M', standard: '岳母', northern: '丈母娘', cantonese: '外母', wu: '丈母', reverse: '女婿' },
131
+ { path: 'H.F', standard: '公公', cantonese: '家翁', wu: '阿公', reverse: '儿媳' },
132
+ { path: 'H.M', standard: '婆婆', cantonese: '家姑', wu: '阿婆', reverse: '儿媳' },
133
+
134
+ // Children's spouses
135
+ { path: 'S.W', standard: '儿媳妇', northern: '儿媳', reverse: '公公/婆婆' },
136
+ { path: 'D.H', standard: '女婿', reverse: '岳父/岳母' },
137
+
138
+ // Siblings' spouses
139
+ { path: 'B+.W', standard: '嫂子', northern: '嫂嫂', cantonese: '大嫂', reverse: '小叔/小姑' },
140
+ { path: 'B-.W', standard: '弟媳', northern: '弟妹', cantonese: '弟妇', reverse: '大伯/大姑' },
141
+ { path: 'Z+.H', standard: '姐夫', cantonese: '姐夫', reverse: '小舅/小姨' },
142
+ { path: 'Z-.H', standard: '妹夫', cantonese: '妹夫', reverse: '大舅/大姨' },
143
+
144
+ // Spouse's siblings
145
+ { path: 'H.B+', standard: '大伯子', northern: '大伯', note: '夫之兄' },
146
+ { path: 'H.B-', standard: '小叔子', northern: '小叔', note: '夫之弟' },
147
+ { path: 'H.Z+', standard: '大姑子', northern: '大姑', note: '夫之姐' },
148
+ { path: 'H.Z-', standard: '小姑子', northern: '小姑', note: '夫之妹' },
149
+ { path: 'W.B+', standard: '大舅子', northern: '大舅哥', note: '妻之兄' },
150
+ { path: 'W.B-', standard: '小舅子', northern: '小舅子', note: '妻之弟' },
151
+ { path: 'W.Z+', standard: '大姨子', northern: '大姨姐', note: '妻之姐' },
152
+ { path: 'W.Z-', standard: '小姨子', northern: '小姨妹', note: '妻之妹' },
153
+
154
+ // Father's brothers' wives
155
+ { path: 'F.B+.W', standard: '伯母', northern: '大娘', cantonese: '伯娘', minnan: '阿婶', wu: '伯娘', reverse: '侄子/侄女' },
156
+ { path: 'F.B-.W', standard: '婶母', northern: '婶婶', cantonese: '婶母', minnan: '阿婶', wu: '阿婶', reverse: '侄子/侄女' },
157
+
158
+ // Father's sisters' husbands
159
+ { path: 'F.Z+.H', standard: '姑父', northern: '姑父', reverse: '外甥/外甥女' },
160
+ { path: 'F.Z-.H', standard: '姑父', northern: '姑父', reverse: '外甥/外甥女' },
161
+
162
+ // Mother's brothers' wives
163
+ { path: 'M.B+.W', standard: '舅母', northern: '大妗', note: '舅父之妻' },
164
+ { path: 'M.B-.W', standard: '舅母', northern: '舅妈', note: '舅父之妻' },
165
+
166
+ // Mother's sisters' husbands
167
+ { path: 'M.Z+.H', standard: '姨父', northern: '姨夫', reverse: '外甥/外甥女' },
168
+ { path: 'M.Z-.H', standard: '姨父', northern: '姨夫', reverse: '外甥/外甥女' },
169
+
170
+ // Grandparents' siblings (祖父/祖母的兄弟姐妹)
171
+ { path: 'F.F.B+', standard: '伯祖父', northern: '老爷爷' },
172
+ { path: 'F.F.B-', standard: '叔祖父', northern: '老爷爷' },
173
+ { path: 'F.F.Z+', standard: '姑祖母', northern: '姑奶奶' },
174
+ { path: 'F.F.Z-', standard: '姑祖母', northern: '姑奶奶' },
175
+ { path: 'F.M.B+', standard: '舅祖父', northern: '舅太爷' },
176
+ { path: 'F.M.Z+', standard: '姨祖母', northern: '姨奶奶' },
177
+ ]
178
+
179
+ /** Build a lookup map for O(1) access */
180
+ export function buildLookupMap(table: KinshipEntry[]): Map<string, KinshipEntry> {
181
+ const map = new Map<string, KinshipEntry>()
182
+ for (const entry of table) {
183
+ map.set(entry.path, entry)
184
+ }
185
+ return map
186
+ }
187
+
188
+ export const KINSHIP_LOOKUP = buildLookupMap(KINSHIP_TABLE)